ART | GC Configuration

之前学习ART GC触发流程时总是一头雾水,根据源码流程可以大致清楚每一次GC完成后会根据当前已经申请的内存大小和以下参数来计算得到一个新的GC水线

  1. dalvik.vm.heaptargetutilization
  2. dalvik.vm.heapmaxfree
  3. dalvik.vm.heapminfree

但是说实话,对于这些参数有什么具体的含义,为什么需要这么复杂的机制总是摸不着头脑。 例如为什么每次GC后需要GrowForUtilization来重新计算水线?这些参数的意义是什么?

偶然间发现了一篇位于art/runtime/目录,名为gc_configuration.md的文档解答了我很多疑惑 art/runtime/gc_configuration.md

heaptargetutilization

GC的次数越少那么应用占用的内存就会越多;而GC的次数越频繁,GC的开销就会越大 。因此GC开销和内存占用之间是不可以兼得的

由于堆的大小不同,衡量GC开销通常用回收每字节垃圾对象所需要的开销来衡量:

假设当前存活的对象大小为 <math xmlns="http://www.w3.org/1998/Math/MathML"> L L </math>L,扫描每个字节所需要的开销为 <math xmlns="http://www.w3.org/1998/Math/MathML"> c c </math>c,GC的总开销可以近似表示为扫描所有存活对象的带来的开销 <math xmlns="http://www.w3.org/1998/Math/MathML"> c L cL </math>cL。将heaptargetutilization简写为 <math xmlns="http://www.w3.org/1998/Math/MathML"> u u </math>u,当Heap增长到 <math xmlns="http://www.w3.org/1998/Math/MathML"> L / u L/u </math>L/u时触发GC可以回收大小为 <math xmlns="http://www.w3.org/1998/Math/MathML"> L / u − L L/u - L </math>L/u−L的垃圾,那么回收每字节垃圾对象所需要的开销等于 <math xmlns="http://www.w3.org/1998/Math/MathML"> c L L / u − L = u c 1 − u \frac{cL}{L/u - L} = \frac{uc}{1-u} </math>L/u−LcL=1−uuc,这是一个和当前存活对象大小无关的结果,并且随着 <math xmlns="http://www.w3.org/1998/Math/MathML"> u u </math>u的增加而增加。

也就是说:

  1. heaptargetutilization越大说明回收每字节垃圾对象所需要的开销越大
  2. heaptargetutilization越大占用的最大堆大小越小( <math xmlns="http://www.w3.org/1998/Math/MathML"> L u \frac{L}{u} </math>uL)

当前的设备上通常将heaptargetutilization设置为0.5

当然这个值的设置需要有一个上界:

当前Concurrent Copying GC中存活对象比例超过75%的region是不需要进行回收的(kEvacuateLivePercentThreshold );Concurrent Mark-Compact GC中是存活对象比例超过95%的region是不需要进行回收的(kBlackDenseRegionThreshold)

也就是说不管是CC算法还是CMC算法,都会产生内存碎片。因此设置的heaptargetutilization应该严格小于对应的Threshold,否则可能会造成每一次回收的垃圾对象很少导致非常频繁的回收

heap max/min free

只依赖 <math xmlns="http://www.w3.org/1998/Math/MathML"> L u \frac{L}{u} </math>uL 的值来触发GC也会存在一些问题:

  • 在 <math xmlns="http://www.w3.org/1998/Math/MathML"> L L </math>L比较小的场景下, <math xmlns="http://www.w3.org/1998/Math/MathML"> L u \frac{L}{u} </math>uL 这个水线会非常容易到达,导致频繁的触发GC
  • 在 <math xmlns="http://www.w3.org/1998/Math/MathML"> L L </math>L比较大的场景下, <math xmlns="http://www.w3.org/1998/Math/MathML"> L u \frac{L}{u} </math>uL 这个水线会变的非常高,导致堆内存的占用比较大

GC还会有一些固定的开销,例如扫描GC roots,通过STW来扫描references等。在正常情况下GC的开销会和 <math xmlns="http://www.w3.org/1998/Math/MathML"> L L </math>L近似的成线性关系,但是在 <math xmlns="http://www.w3.org/1998/Math/MathML"> L L </math>L非常小的场景下,每次GC的固定开销可能就不能忽视了。假设当前的 <math xmlns="http://www.w3.org/1998/Math/MathML"> L = 0 L=0 </math>L=0,那么每次分配一次对象就会触发一次GC,这显然是不太合理的,因此需要heapminfree来保证触发GC的最低水线。最理想的场景下heapminfree也是应该需要根据固定的开销来进行一个动态的更新,但是在ART中当前的heapminfree还是一个经验值。

在应用的堆大小已经非常大的场景下,如果当前的业务需要尽可能的限制其堆空间的进一步增长,可以通过设置heapmaxfree来保证触发GC的最高水线。这个值在设备的物理内存快速增长的场景下可能不太具备有现实意义了,Google的ART团队建议将这个值设置为maximum heap size,在将来可能会移除这个参数。

总结

现在可以回答开头的一些问题:

这些GC的参数都是在GC开销和内存占用中寻找一个平衡点

  1. heaptargetutilization作为一个基准值来确定GC开销和内存占用
  2. heapminfree用来在HeapSize较小的场景下抑制GC,通过增加内存占用减少GC开销
  3. heapmaxfree用来在HeapSize较大的场景下促进GC,通过增加GC开销来减少内存占用

每次GC完成后,可以认为当前已经分配的对象都是存活的对象,根据上面三个参数动态的计算出下一次的GC水线,保证GC的开销和内存占用都处在一个可控的范围内

相关推荐
SharpCJ4 小时前
Android 开发者为什么必须掌握 AI 能力?端侧视角下的技术变革
android·ai·aigc
_李小白5 小时前
【OSG学习笔记】Day 38: TextureVisitor(纹理访问器)
android·笔记·学习
JJay.5 小时前
Kotlin 高阶函数学习指南
android·开发语言·kotlin
jinanwuhuaguo5 小时前
截止到4月8日,OpenClaw 2026年4月更新深度解读剖析:从“能力回归”到“信任内建”的范式跃迁
android·开发语言·人工智能·深度学习·kotlin
JJay.6 小时前
Android Kotlin 协程使用指南
android·开发语言·kotlin
BLUcoding6 小时前
Android 布局介绍
android
summerkissyou19876 小时前
android-蓝牙-状态和协议值总结及监听例子
android·蓝牙
徒 花6 小时前
数据库知识复习05
android·数据库
提子拌饭1339 小时前
番茄时间管理:鸿蒙Flutter 实现的高效时间管理工具
android·flutter·华为·架构·开源·harmonyos·鸿蒙
4311媒体网9 小时前
帝国CMS二次开发实战:精准实现“最新资讯”标识与高亮判断
android