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();,底层到底干了啥?
- 指令解析:Dalvik字节码遇到
new-instance指令,开始分配流程。 - 类查找:检查目标类是否已加载、已初始化。没加载?先走类加载流程。
- 计算大小:根据类信息算出对象需要多少字节(包括对象头)。
- 分配内存:调用dlmalloc分配一块连续内存。
- 对象头初始化:写入锁状态、GC标记、类引用等信息。
- 执行构造方法:调用
方法,完成初始化。
这里有个细节:对象头在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大小)直接走全局分配
我做个图帮你理解:
TLAB的好处很明显:
- 无锁分配:线程内部操作,零竞争
- 分配速度快:就一个指针移动,比dlmalloc的链表查找快得多
- 减少全局锁争用:全局堆的锁压力大大降低
但TLAB也有代价:
- 内存浪费:每个线程的TLAB内部可能有碎片,线程退出时TLAB未用完的空间就浪费了
- 不适合大对象:大对象直接走全局堆,避免TLAB被撑爆
避坑指南:我曾经在一个多线程图片处理应用里,发现GC频繁。后来一查,是每个线程都创建了大量临时byte数组,TLAB频繁耗尽,导致线程反复向全局堆申请新TLAB。解决方案是:用线程池复用线程,减少TLAB的创建销毁开销。
4.4 总结一下
Dalvik的内存分配机制,说白了就是分层管理:
| 组件 | 管理内容 | 特点 |
|---|---|---|
| dlmalloc | Java对象堆 | 通用分配,支持释放,有碎片 |
| LinearAlloc | 类元数据 | 线性分配,不释放,空间有限 |
| TLAB | 线程本地对象 | 无锁分配,速度快,有浪费 |
嗯,这块内容就讲到这儿。你理解了这些底层机制,以后做内存优化、排查OOM,心里就有底了。记住一句话:分配越简单,性能越好;但简单往往意味着浪费。 这就是工程上的取舍。
公众号:蓝海资料掘金营,微信deep3321