之前学习ART GC触发流程时总是一头雾水,根据源码流程可以大致清楚每一次GC完成后会根据当前已经申请的内存大小和以下参数来计算得到一个新的GC水线
- dalvik.vm.heaptargetutilization
- dalvik.vm.heapmaxfree
- 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的增加而增加。
也就是说:
heaptargetutilization
越大说明回收每字节垃圾对象所需要的开销越大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开销和内存占用中寻找一个平衡点
heaptargetutilization
作为一个基准值来确定GC开销和内存占用heapminfree
用来在HeapSize较小的场景下抑制GC,通过增加内存占用减少GC开销heapmaxfree
用来在HeapSize较大的场景下促进GC,通过增加GC开销来减少内存占用
每次GC完成后,可以认为当前已经分配的对象都是存活的对象,根据上面三个参数动态的计算出下一次的GC水线,保证GC的开销和内存占用都处在一个可控的范围内