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的开销和内存占用都处在一个可控的范围内

相关推荐
沙振宇3 小时前
【HarmonyOS】ArkTS开发应用的横竖屏切换
android·华为·harmonyos
橙子199110165 小时前
Kotlin 中的作用域函数
android·开发语言·kotlin
zimoyin5 小时前
Kotlin 懒初始化值
android·开发语言·kotlin
枣伊吕波6 小时前
第六节第二部分:抽象类的应用-模板方法设计模式
android·java·设计模式
萧然CS6 小时前
使用ADB命令操作Android的apk/aab包
android·adb
_extraordinary_10 小时前
MySQL 事务(二)
android·数据库·mysql
鸿蒙布道师14 小时前
鸿蒙NEXT开发动画案例5
android·ios·华为·harmonyos·鸿蒙系统·arkui·huawei
橙子1991101619 小时前
在 Kotlin 中什么是委托属性,简要说说其使用场景和原理
android·开发语言·kotlin
androidwork19 小时前
Kotlin Android LeakCanary内存泄漏检测实战
android·开发语言·kotlin
笨鸭先游20 小时前
Android Studio的jks文件
android·ide·android studio