4、Dalvik内存分配机制:dlmalloc与LinearAlloc,对象分配流程,TLAB(线程本地分配缓冲区)原理

好,咱们今天来聊聊Dalvik虚拟机里头的内存分配。说实话,这块内容我当年啃源码的时候也绕了不少弯路。你想想看,一个Java对象new出来,底层到底是怎么给它腾地方的?Dalvik不像JVM那样有分代收集,它的分配器设计得更贴近嵌入式场景。嗯,咱们一个一个拆开说。

4.1 两大分配器:dlmalloc 和 LinearAlloc

Dalvik里头的内存分配,说白了就两套班子在干活。一套是dlmalloc,另一套是LinearAlloc。它们俩分工很明确,各管各的地盘。

4.1.1 dlmalloc——通用堆分配器

dlmalloc 是从C语言社区移植过来的,全称是 Doug Lea's Malloc。它负责管理Dalvik堆里头的Java对象内存。我早期做性能优化时,就经常盯着dlmalloc的行为看——它用了一个叫空闲链表的数据结构来管理碎片。

它的核心逻辑其实不复杂:

  • 维护多个大小不同的空闲块链表(bins)
  • 分配时找最合适大小的块(best-fit策略)
  • 释放时合并相邻空闲块,减少碎片

举个例子,你new一个byte[1024],dlmalloc就会去对应的bin里找一块不小于1024字节的空闲内存。找不到就向系统申请更大的内存块,切一部分给你,剩下的挂回链表。

关键点:dlmalloc分配的内存地址是随机的,这会导致碎片。但好处是灵活性高,大小随意。

4.1.2 LinearAlloc——线性分配器

LinearAlloc 就简单粗暴多了。它只干一件事:顺序分配,从不释放。你想想看,这玩意儿用在哪儿?

答案是——类加载。每个类加载时,它的常量池、方法信息、字段信息这些元数据,都扔到LinearAlloc里。因为这些数据一旦加载,基本就不会卸载(除非整个ClassLoader被回收)。

我当年调试一个插件化框架时,就遇到过LinearAlloc耗尽的问题。那时候Android 4.x的LinearAlloc默认只有8MB,插件一多就崩。后来Android 5.0把这个值提到了16MB,才算缓解。

个人经验:如果你在做热修复或者插件化,一定要留意LinearAlloc的剩余空间。可以用Runtime.getRuntime().maxMemory()间接估算,但更准的是看/proc/self/maps里头的线性分配区段。

4.2 对象分配流程——从new到内存落地

好,咱们走一遍完整的对象分配流程。你写一句Object obj = new Object();,底层到底干了啥?

  1. 指令解析:Dalvik字节码遇到new-instance指令,开始分配流程。
  2. 类查找:检查目标类是否已加载、已初始化。没加载?先走类加载流程。
  3. 计算大小:根据类信息算出对象需要多少字节(包括对象头)。
  4. 分配内存:调用dlmalloc分配一块连续内存。
  5. 对象头初始化:写入锁状态、GC标记、类引用等信息。
  6. 执行构造方法:调用方法,完成初始化。

这里有个细节:对象头在Dalvik里是4字节(32位)或8字节(64位)。它存了锁状态、哈希码、GC年龄等信息。我见过不少开发者忽略对象头的大小,导致内存估算偏差很大。

注意:Dalvik的对象头跟JVM不一样。JVM用两个word(mark word + klass pointer),Dalvik用一个word搞定所有。所以Dalvik的对象开销更小,但能存的信息也少。

4.3 TLAB——线程本地分配缓冲区

TLAB,全称 Thread-Local Allocation Buffer。这玩意儿是Dalvik在Android 5.0(ART)之后才引入的。它的目的很简单:减少线程竞争

你想想看,多线程同时new对象,大家都去抢dlmalloc的锁,那性能得多差?TLAB的思路是:给每个线程预先分配一小块内存(比如几百KB),线程内部new对象时,直接从自己的TLAB里切,不用加锁。

TLAB的工作原理:

  • 每个线程维护一个bump pointer(游标指针)
  • 分配时直接移动指针,像切蛋糕一样
  • TLAB用完了,再向全局堆申请新的TLAB
  • 大对象(超过TLAB大小)直接走全局分配

我做个图帮你理解:

全局堆(Global Heap)—— dlmalloc管理 空闲链表 | 已分配块 | 大对象区域 加锁访问,线程安全 线程1 TLAB 已分配 空闲空间 bump pointer → 指向空闲起始 线程2 TLAB 已分配 空闲空间 线程3 TLAB 已分配 空闲空间 大对象(超过TLAB大小)→ 直接走全局堆 例如:大数组、Bitmap等 避免TLAB浪费,直接全局分配

TLAB的好处很明显:

  • 无锁分配:线程内部操作,零竞争
  • 分配速度快:就一个指针移动,比dlmalloc的链表查找快得多
  • 减少全局锁争用:全局堆的锁压力大大降低

但TLAB也有代价:

  • 内存浪费:每个线程的TLAB内部可能有碎片,线程退出时TLAB未用完的空间就浪费了
  • 不适合大对象:大对象直接走全局堆,避免TLAB被撑爆

避坑指南:我曾经在一个多线程图片处理应用里,发现GC频繁。后来一查,是每个线程都创建了大量临时byte数组,TLAB频繁耗尽,导致线程反复向全局堆申请新TLAB。解决方案是:用线程池复用线程,减少TLAB的创建销毁开销。

4.4 总结一下

Dalvik的内存分配机制,说白了就是分层管理

组件 管理内容 特点
dlmalloc Java对象堆 通用分配,支持释放,有碎片
LinearAlloc 类元数据 线性分配,不释放,空间有限
TLAB 线程本地对象 无锁分配,速度快,有浪费

嗯,这块内容就讲到这儿。你理解了这些底层机制,以后做内存优化、排查OOM,心里就有底了。记住一句话:分配越简单,性能越好;但简单往往意味着浪费。 这就是工程上的取舍。


公众号:蓝海资料掘金营,微信deep3321