*CMS和G1

CMS CMS(concurrent mark sweep) 是一种以获取最短回收停顿时间为目标的垃圾收集器。这是因为CMS在工作时可以与用户线程并发执行,以此来降低停顿时间。
CMS用于老年代回收,使用标记-清除算法。
为什么不采用标记-整理算法?

  • cms 主要核心是降低停顿时间,清除垃圾时,应用程序还在运行,如果采用整理的方式,设计到存活对象的移动,此时不停顿很难处理,这样会加大停顿时间。
  • cms主要用于老年代,而老年代的对象可能会长时间存活且存活率高,再或者就是一些大对象,那么对这些对象的移动是不划算的。
这里对比标记-清除,标记-整理,和复制三种算法
标记-清楚 标记-整理 复制
速度
空间开销 少(有内存碎片) 少(没有内存碎片) 大(无碎片)
移动对象

流程
  • 初始标记
    • stop the world
    • 只是标记 gc roots直连的对象,速度快。
  • 并发标记
    • 进行gc roots tracing的过程
  • 重新标记
    • stop the world
    • 修正并发标记过程因用户程序导致的变动
  • 并发清除
    • 并发的清除
为什么需要两次stw
以方便后续gc线程与业务线程并发的执行后续的间接标记存活对象;再之后大部分对象都被标记了,后续的第二次停顿是为了彻底标记之前被遗漏的部分,就是在这一阶段业务线程产生的存活对象进行标记,当然也区分出了死亡对象。虚拟机通过在一些特定指令位置设置“安全点”,当程序运行到安全点是就会暂停锁运行的线程。
安全点 程序执行时并非在任意地方都可以停顿下来开始gc,只有在安全点才能停顿。
安全点的目的不是让程序停下来,而是找到一个稳定的状态,保证java堆栈不会发生变化,能够保证gc的安全可达性分析。
安全点的选定是以程序“是否具有让程序长时间执行特征”为标准选定的。“长时间执行”的最明显特征就是指令序列复用,比如 方法调用,循环跳转。
如何让gc发生时,所有线程都“跑”到安全点上再停顿下来?
  • 抢占式中断:在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。现在几乎没有虚拟机采用这种方式来暂停线程从而响应GC事件。
  • 主动式中断:当GC需要中断线程的时候,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。轮询标志的地方和安全点是重合的,另外再加上创建对象需要分配内存的地方。
安全区域:指在一段代码片段中,引用关系不会发生变化。在这个区域中任意地方开始GC都是安全的。也可以把Safe Region看作是被扩展了的Safepoint。

卡表(Card Table) 老年代的对象可能引用新生代的对象,那标记存活对象的时候,需要扫描老年代中的所有对象。也就是minor gc 可能去扫描老年代?
卡表就是解决这种场景:该技术将整个堆划分为一个个大小为512字节的卡,并且维护一个卡表,用来存储每张卡的一个标识位。这个标识位代表对应的卡是否可能存有指向新生代对象的引用。如果可能存在,这张卡是脏的。
在进行Minor GC的时候,我们便可以不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中的对象加入到Minor GC的GC Roots里。当完成所有脏卡的扫描之后,Java虚拟机便会将所有脏卡的标识位清零。
想要保证每个可能有指向新生代对象引用的卡都被标记为脏卡,那么Java虚拟机需要截获每个引用型实例变量的写操作,并作出对应的写标识位操作。
卡表能用于减少老年代的全堆空间扫描,这能很大的提升GC效率。

G1 G1重新定义了堆空间,打破了原有的分代模型,将堆划分为一个个区域Region,Region是一块地址连续的内存空间,。这么做的目的是在进行收集时不必在全堆范围内进行,这是它最显著的特点。
区域划分的好处就是带来了停顿时间可预测的收集模型:用户可以指定收集操作在多长时间内完成。即G1提供了接近实时的收集特性。G1会通过一个合理的计算模型,计算出每个Region的收集成本并量化,这样一来,收集器在给定了“停顿”时间限制的情况下,总是能选择一组恰当的Regions作为收集目标,让其收集开销满足这个限制条件,以此达到实时收集的目的。
【*CMS和G1】Region的大小是一致的,数值是在1M到32M字节之间的一个2的幂值数,JVM会尽量划分2048个左右、同等大小的Region,
特点:
  • 并行与并发:
    • G1线程可以和用户线程并发执行。
  • 分代收集
  • 空间整合:
    • 与CMS的标记-清除算法不同,G1从整体来看是基于标记-整理算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿:
    • 降低停顿时间是G1和CMS共同的关注点,但是g1提供可预测的停顿时间
流程:
  • 初始标记
    • 只标记gc roots 直连的对象
    • 并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象
    • 这阶段需要停顿线程,但耗时很短。
  • 并发标记
    • gc roots tracing 工程,标记存活的对象,
    • 耗时较长,可以用户程序并发执行,也会扫描SATB pre-write barrier 记录的引用;
  • 最终标记
    • 修正并发标记过程中变动的对象。
    • 虚拟机将并发标记阶段对象变化记录在线程的RememberedSetLog中,最终标记阶段需要把RememberedSet Logs的数据合并到RememberSet中。
    • 需要停顿线程,但是可并行执行
    • 跟cms中重新标记对应,但是本质是不同的,g1的最终标记是更轻量的,他只需要 flush SATB pre-write barrier 的buffer。
  • 筛选回收
    • 对各个Region 的回收成本和价值排序,根据用户期望的 gc停顿时间 制定计划回收。
    • 这个阶段也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。
    • 重置rset
Remembered Set: 在G1中,引入了一种新的能加入根集合的类型,就是记忆集(Remembered Set)。Remembered Sets(也叫RSets)用来跟踪对象引用。G1的很多开源都是源自Remembered Set,例如,它通常约占Heap大小的20%或更高。并且,我们进行对象复制的时候,因为需要扫描和更改Card Table的信息,这个速度影响了复制的速度,进而影响暂停时间。
卡表结构的变种,卡表是我引用了谁,RSet通常是谁引用了我,RSet记录了其他Region中的对象引用本Region中对象的关系。RSet的价值在于使得垃圾收集器不需要扫描整个堆找到谁引用了当前分区中的对象,只需要扫描RSet即可减少全堆扫描,gc过程可以借助RSet作为根基扫描存活对象,。
SATB SATB(Snapshot-At-The-Begin),就是在初始标记开始时,G1 收集器打了一个快照,形成一个所谓的对象图 (Object Graph)。标记数据结构包括了两个位图:previous位图和next位图。previous位图保存了最近一次完成的标记信息,并发标记周期会创建并更新next位图,随着时间的推移,previous位图会越来越过时,最终在并发标记周期结束的时候,next位图会将previous位图覆盖掉

两种模式: yang gc :新生代垃圾收集 mixed gc :混合垃圾收集

    推荐阅读