查看原文
其他

Android ART虚拟机 内存分配的原理

鸿洋
2024-08-24

The following article is from 半行代码 Author 半行代码

最近看了下art虚拟机的内存分配原理,在这里简要的分享一下。在art虚拟机里,维护了很多个空间分配内存,这些内存空间在art的源码里面被抽象成一个个Space对象。类之间的关系我从网上找了以下这张图来表示,非常清晰:


Space根据内存空间是否连续分为两类:
  • ContinuousSpace 内存空间连续,这里主要代表有两种:
    • ImageSpace 这表示的是系统镜像的内存空间,我们一般关注不到。
    • CntinuiusMemMapAllocSpace的几个子类,这些Space都是一些不同的内存分配策略,对应的则是虚拟机不同的GC策略。根据文档和一些网上可以查到的资料,我们一般也就关心 RegionSpace 和 MallocSpace。
  • DisContinuousSpace 内存空间不连续,主要代表就是 LargeObejctSpace(大对象)
1小对象分配

小对象分配会根据我们的GC回收算法来指定我们的分配策略。具体的一个对应关系可以参考我整理的这个图:


在AndroidO之后,Android默认指定的垃圾回收算法是CC(并发复制)算法,在CC之前规则为,前台使用的是 CMS(并发标记清除),后台使用的空间压缩算法。所以对我们来说,主要关心的就是两种:
  • KCollectorTypeCMS
  • KCollectorTypeCC
这2个gc的算法核心还是复制算法和标记清除算法,同时支持了和程序并发运行。我找了两张图,回顾一下这两种算法:
  1. 复制算法
复制算法在回收内存空间的时候,回收器会把内存分为大小相等的两部分,在gc发生的时候,会把存活的对象从源空间复制到目标空间,复制结束后,所有的存活对象都排布在目标空间。然后把源空间全部清除。这是一个典型的空间换时间的策略。


  1. 标记清除算法
标记清除算法就是标记出需要回收的对象,标记完成之后统一进行回收。标记清除的缺陷就是内存碎片空间会变多,在之后分配内存的时候容易在对象连续内存空间不够的时候触发频繁gc。


回顾完 CMS 和CC,我们继续看相关的分配策略,这里分别对应了:
  • KAllocatorTypeRegionTLAB、KAllocatorTypeRegion
    • 这里根据是否支持TLAB来决定,实测证明现在手机用的 KAllocatorTypeRegionTLAB,这个是高版本Android默认的小对象分配策略。
  • KAllocatorTypeRosAlloc、KAllocatorTypeDlMalloc
    • DlMalloc实际上内存分配就是用的C语言的malloc,在art虚拟机里,Google替换成了自己的 ROS (runs-of-slots) 算法,所以现在大部分使用场景是 ROS的。
RegionAlloc
在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
RosAlloc就是一个魔改版本的malloc,RosAlloc维护了一个Slot链表,每个Slot叫做Run,每个Slot有自己的内存size,可以灵活分配。这个算法的优点就是内存分配精确,能减少碎片化,缺点则是需要不适合分配大对象。关于RosAlloc的代码我就不分析了,感觉没什么必要,网上随便找个过程的图:


2大对象分配


除了普通小对象,还有一个很重要的对象分配空间就是大对象了。

当满足大对象分配条件的时候会走 AllocLargeObject 逻辑。

当分配字节数大于阈值,并且分配对象是基本类型数组或者字符串,那就符合大对象的条件。阈值是kMinLargeObjectThreshold,即3页大小,每页是4096,也就是4k。所以说一个基础类型数组或者字符串大于 12k,就属于大对象了,就会分配到 LargeObjectSpace 里面。

LargeObjectSpace有两种实现,分别是 FreeListSpaceLargeObejctMapSpace:

这里看的话,如果手机cpu架构是arm64的,使用的是FreeListSpace,否则是 LargeObejctMapSpace。具体分配逻辑在他们各自的 Alloc 函数里:
  • LargeObjectMapSpace

直接使用mmap匿名映射一块内存。
  • FreeListSpace
FreeListSpace则是在初始化内存空间的时候,直接mmap了一定size的内存使用,然后给内存空间分页,这些页被维护在一个链表里面,内存分配的时候会先去查找有无地址和大小一样的空间,有就会复用,没有的话就会开辟新的页去进行分配。

这里的size代码里面追溯一下可以追溯到 Heap 的 growth_limit, 也就是虚拟机参数 -XX:HeapGrowthLimit=_:

这里可以理解成:在arm64架构的设备上,LargeObejctSpace分配内存的最大限制和应用进程的最大堆内存一致。在非arm64架构的设备上,LargeObejctSpace内存分配没有指定的限制大小。

3总结

简单总结了一下art虚拟机内存分配的原理,通过这些点我们可以对安卓里这些java对象如何分配有一个简单的认知,对排查内存相关问题,研究一些内存性能优化方案建立一个基础。


最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


推荐阅读

Android  绘制缓冲实现「黑客帝国代码雨」
Android稳定性:OOM原理解析
Android 性能:FD监控实现原理


扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

继续滑动看下一个
鸿洋
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存