标记清除算法概述
标记清除的思路非常简单:基于链表式的内存管理方式,标记所有内存空间中的活跃对象,同时记录这些活跃对象。在标记完成后,遍历整个内存空间,如果发现内存块中的对象是活跃对象,则不处理;如果发现内存块中的对象是死亡对象,则将内存块放入空闲列表中供后续分配使用。
CMS老生代的回收将标记清除算法进行了优化,减少了垃圾回收导致的停顿时间。其思路是将标记和清除尽可能地并发化,算法被设计为:初始标记、并发标记、再标记和清除等步骤。
1)初始标记(Initial Mark):从根集合出发,标记老生代中的活跃对象。在初始标记中仅仅找到根集合中的引用对象,并不继续递归遍历这些对象的成员变量,而是把这些对象作为并发标记的输入,使用额外的数据结构(标记位图)记录活跃对象。
2)并发标记(Concurrent Mark):根据初始标记的输入,对活跃对象遍历成员变量,根据对象之间的引用关系找到所有活跃对象。
3)预清理和可终止预清理(preClean&abortable-preClean):在并发标记过程中,对象的引用关系可能发生变化,为了保证标记的正确性,可使用卡表等数据结构来记录需要再次标记的对象。但是并发标记过程中可能产生了大量需重新标记的对象,所以引入预清理和可终止预清理步骤,尽量将需要重新标记的对象进行并发标记。
4)再标记(Remark/Final Mark):由于并发标记、并发预清理、并发可终止预清理都有可能引入需要重新标记的对象,因此必须引入一个暂停阶段,停止Mutator修改对象引用关系,并且在该阶段再次从根集合出发,标记老生代中的活跃对象。由于大量的对象已经完成了标记,所以再标记时一般只需要访问对象的标记状态而不需要重新做标记动作,因此停顿时间一般不会太长。注意,再标记时会完全遍历对象的引用关系,不会因为某一个对象被标记过就不遍历其成员变量。至此,老生代中所有的活跃对象都会在标记位图中。
5)并发清除(Concurrent Sweep):遍历老生代,根据标记位图的信息,如果对象已经死亡(没有标记),则回收其占用的内存,如果对象活跃,则不处理。需要注意的是,并发清除中Mutator可能在老生代中分配新的对象,这些对象需要被识别处理并作为活跃对象(简单的处理方法为,当在老生代中分配对象时,会在标记位图中记录新分配的对象)。
6)并发复位(Concurrent Reset):最后尝试扩展老生代、重新复位和清除一些数据结构(例如清除标记位图),方便下一次执行垃圾回收。
关于如何保证并发标记的正确性在前文已经讨论过,这里不赘述。下面详细介绍一下每一步完成的工作,以及实现过程中的一些细节问题。
并发算法触发时机
老生代的回收有主动回收也有被动回收。其中主动回收也称为后台GC,它由CMS控制线程判断是否需要触发。与后台GC对应的是前台GC,前台GC是指在后台GC发生时又从Mutator触发了GC,因为前台GC触发时需要与后台GC进行交互,所以行为更为复杂。
我们先来研究后台GC,再来介绍前台GC。后台GC只有满足一定的条件时才会尝试触发,其前提条件如下:
1)参数CMSWaitDuration大于等于0时(默认值为2000),表示当间隔CMSWaitDuration毫秒无任何GC或者在间隔时间内有Minor GC执行,可以尝试触发。
2)如果参数CMSWaitDuration小于0,则每隔CMSCheckInterval毫秒可以尝试触发,参数CMSCheckInterval的默认值为1000。
是否能触发,还需要看是否满足下面的条件,只有满足下面的条件时才会真正触发回收,否则直接放弃回收。主要的触发条件如下:
1)如果发现了Mutator的代码中通过GC Locker或者System GC等方式要求触发并发回收,则直接执行。注意,这两种行为通过参数GCLockerInvokesConcurrent和
ExplicitGCInvokesConcurrent控制,默认情况下均为false,即不会触发。
2)参数
UseCMSInitiatingOccupancyOnly(默认为false)为false时,推断并发回收执行完时是否用尽老生代空间,即老生代内存是否已经紧张。
如果推断内存已经紧张,则会直接触发后台GC;如果推断内存并不紧张,但是内存的使用超过了一定的阈值(由参数CMSBootstrapOccupancy控制,默认值是50,表示老生代的50%空间),也会直接触发。
3)如果老生代内存的使用超过一定阈值也会触发,阈值的计算方式如下。
CMSInitiatingOccupancyFraction设置为大于0,表示老生代空间的使用超过该阈值时直接触发。默认值为-1,表示并不使用该规则。
其中MinHeapFreeRatio默认值为40,CMSTriggerRatio默认值为80,得到默认值为92。
4)如果
CMSInitiatingOccupancyFraction为false,并且发现扩展内存可能导致的GC时,也会触发,否则忽略。
5)如果已经发生Minor GC晋升失败或者发现老生代的剩余空间不足以满足下一次Minor GC的晋升时也会触发。
6)元数据发生过分配失败,但还没有触发过GC,此时也会触发老生代回收。
7)以上条件均不满足时,如果参数CMSTriggerInterval设置为大于0,则会计算从上次回收到现在的时间间隔是否大于该参数,如果大于则执行。
参数CMSTriggerInterval的默认值为-1,表示规则不生效。
从后台GC触发的规则可以看出,JVM设计者还是想尽量避免用户主动调参,希望通过预测机制来触发GC的执行。但是在一些特殊场景中,Mutator分配请求的突变可能导致预测方法失效,在这种情况下,用户可以通过设置相关的参数尽早地触发后台GC。