最近看了下art虚拟机的内存分配原理,在这里简要的分享一下。在art虚拟机里,维护了很多个空间分配内存,这些内存空间在art的源码里面被抽象成一个个Space对象。类之间的关系我从网上找了一下张图来表示,非常清晰:
Space根据内存空间是否连续分为两类:
小对象分配会根据我们的GC回收算法来指定我们的分配策略。具体的一个对应关系可以参考我整理的这个图:
在AndroidO之后,Android默认指定的垃圾回收算法是CC(并发复制)算法,在CC之前规则为,前台使用的是 CMS(并发标记清除),后台使用的空间压缩算法。所以对我们来说,主要关心的就是两种:
这2个gc的算法核心还是复制算法和标记清除算法,同时支持了和程序并发运行。我找了两张图,回顾一下这两种算法:
复制算法在回收内存空间的时候,回收器会把内存分为大小相等的两部分,在gc发生的时候,会把存活的对象从源空间复制到目标空间,复制结束后,所有的存活对象都排布在目标空间。然后把源空间全部清除。这是一个典型的空间换时间的策略。
标记清除算法就是标记出需要回收的对象,标记完成之后统一进行回收。标记清除的缺陷就是内存碎片空间会变多,在之后分配内存的时候容易在对象连续内存空间不够的时候触发频繁gc。
回顾完 CMS 和CC,我们继续看相关的分配策略,这里分别对应了:
在heap的TryToAllocate函数里面,判断到 RegionAlloc:
分别看下Region和RegionTLAB:
这段代码还比较好理解,把内存分成了一个一个Region,通过Region去分配对应的内存。每个Region有固定的大小(kRegionSize(256kb))。当一个Region分配对象不够,我们就先分配一个Region出来。通过Region分配对象内存,核心代码就是下图圈出来的部分,用top指针维护当前分配到哪里:
当对象分配的内存需要大于1个Region的时候,会按Region分配里的LargeObject去处理(需要注意这里的大对象和largeObjectSpace的大对象不是一回事),LargeObject处理比较复杂,大概意思就是通过创建多个Region去分配,具体这里不深入研究。RegionAllocTLAB则是每个线程会分配一个空间,如果tlab分配失败则会按照RegionAlloc的方式去分配
TLAB就是每个线程自己的内存区域,这个和java里的ThreaLocal应该是同一种思想。减少内存竞争,提高内存分配的效率。
RosAlloc就是一个魔改版本的malloc,RosAlloc维护了一个Slot链表,每个Slot叫做Run,每个Slot有自己的内存size,可以灵活分配。这个算法的优点就是内存分配精确,能减少碎片化,缺点则是需要不适合分配大对象。关于RosAlloc的代码我就不分析了,感觉没什么必要,网上随便找个过程的图:
除了普通小对象,还有一个很重要的对象分配空间就是大对象了。
当满足大对象分配条件的时候会走 AllocLargeObject 逻辑。
当分配字节数大于阈值,并且分配对象是基本类型数组或者字符串,那就符合大对象的条件。阈值是kMinLargeObjectThreshold,即3页大小,每页是4096,也就是4k。所以说一个基础类型数组或者字符串大于 12k,就属于大对象了,就会分配到 LargeObjectSpace 里面。
LargeObjectSpace有两种实现,分别是 FreeListSpace和 LargeObejctMapSpace:
这里看的话,如果手机cpu架构是arm64的,使用的是FreeListSpace,否则是 LargeObejctMapSpace。具体分配逻辑在他们各自的 Alloc 函数里:
直接使用mmap匿名映射一块内存。
FreeListSpace则是在初始化内存空间的时候,直接mmap了一定size的内存使用,然后给内存空间分页,这些页被维护在一个链表里面,内存分配的时候会先去查找有无地址和大小一样的空间,有就会复用,没有的话就会开辟新的页去进行分配。
这里的size代码里面追溯一下可以追溯到 Heap 的 growth_limit, 也就是虚拟机参数 -XX:HeapGrowthLimit=_
:
这里可以理解成:在arm64架构的设备上,LargeObejctSpace分配内存的最大限制和应用进程的最大堆内存一致。在非arm64架构的设备上,LargeObejctSpace内存分配没有指定的限制大小。
简单总结了一下art虚拟机内存分配的原理,通过这些点我们可以对安卓里这些java对象如何分配有一个简单的认知,对排查内存相关问题,研究一些内存性能优化方案建立一个基础。