学习过JAVA语言的堆CMS这款垃圾收集器都不会陌生,CMS曾经号称是并发度最高的垃圾收集器。CMS是一款只能应用于老年代收集的垃圾收集器。CMS为了支持与应用线程同时工作(垃圾收集的时候,业务线程同时工作,修改对象),重载了写屏障(赋值引用对象被修改的时候,将其压入标记栈)代码。在并发标记阶段修改的对象必须重新标记使得所有的对象都被标记了。
垃圾收集器可以简化内存分配和增强鲁棒性,但是早期不被程序员所接受,很大一部原因是性能问题。开发者不接收自动垃圾回收,只有两方面的原因:吞吐量和延迟。计算能力的增加被内存需求增加所抵消了。
分代收集可以较好解决吞吐量和延迟的问题?如何解决呢?将整个堆划分成两部分,新生代和老年代。
CMS充分利用分代收集系统的优势,致力于减少最糟糕的情形下垃圾回收的停顿时间,它在大部分的情形下可以和业务线程同时运行,只有极少情况下会挂起业务线程。
cms是一个并发的三色算法,该算法使用写屏障,将变更的对象保持为灰色。cms在三色算法的基础上做了一些创新,牺牲了完全并发以获得更高的吞吐量, 它允许在堆根节点变更时不需要保证三色的不变,对根节点(栈,寄存器,全局变量)的更新比堆中的更新通常更频繁。该算法在处理根节点时,会短暂的挂起应用线程,该算法假设在一个堆中对象的变更频率较低的基础之上,否则,在重新标记阶段需要扫描大量的脏对象,导致较长的停顿时间。虽然某些程序会打破我们的假设,但是,Boehm et al.的报告中显示在实践中这项技术运行良好,尤其是在交互式的应用中。主要由4阶段组成:
该示例取自一篇牛逼的论文,解释我们的场景完全足够。整个堆内存有4页,包含了7个对象。在初始标记阶段,4页都标记为clean,对象a是从根直接可达的,所以将其标记为活对象。
1a处于并发标记的过程中,对象b,c,e都被标记为活对象。在这个时候,对象g应用d被删除了,对象b引用c修改为引用d.因为g和b发生了变更,所以第1页和第3页被标记为脏页。
1c表示在并发标记结束时的样子。很明显,标记还不完整,因为b的引用对象d还没有被标记。在重新标记阶段才会被标记:在这个阶段所有的脏页会重新扫描,d会被标记上。
1d表示就是重新标记后的状态,这时候标记就结束了。下一个阶段就是并发清除了,最终f会被回收。
在回收的时候,虽然c现在是不可达的对象,但它被标记了,所以不会被回收,它会在下一次垃圾回收的时候会被回收。
有如下的集中方式,CMS选择了空闲列表的方式。
某些场景下,分代收集器需要跟踪老年代到新生代的引用。CMS使用卡表的方式来解决这个问题。
标记对象使用额外的bitmap来存储,没有直接存储在对象头中。避免并发过程中,影响对象头的访问。对对象的扫描需要一个额外的数据结构来存储将要被扫描的对象,队列或者栈来存储。
随着堆内存分配和回收,内存块的大小会逐渐变小,清除回收之后,需要堆内存块进行合并操作。在非并发的场景,可以直接将所有的空闲列表的空间直接重建就能实现。
在并发收集器中,回收的同时也在做内存分配,这个加排它锁可以解决。
initFrac = (1 - heapOccupancyFrac) * allocBeforeCycleFrac;
while (TRUE) {
sleep(SLEEP_INTERVAL);
if (generationOccupancy() > initFrac) {
/* 1st stop-the-world phase */
initialMarkingPause();
concurrentMarkingPhase();
concurrentPreCleaningPhase();
if (markedPercentage() < 98%) {
/* 2nd stop-the-world phase */
finalMarkingPause();
if (markedPercentage() < 98%)
concurrentSweepingPhase();
}
}
}
原文:https://www.cnblogs.com/dragonfei/p/13747539.html