JVM三种GC流程比较


一、新生代垃圾回收(Serial/ParallelScavenge/ParaNew/G1)

新生代使用复制算法来回收死亡对象

1.1 根据GC ROOT标记存活对象

不管那种GC流程,做的第一步都是先遍历找到所有的GC ROOT,然后再根据GC ROOT往下遍历,标记出所有存活的对象。要注意标记的是存活的对象,也就是在最终进行GC的时候回收的是未被标记的对象。

那么是如何标记的,如果使用GC收集器是CMS/G1的话,采用的标记算法就是三色标记算法,存活的对象会被标记为黑色,还未遍历完不确定的对象标记为灰色,死亡(即没被外部持有引用的对象)被标记为白色。

另外还有一个问题,什么是GC ROOT呢?一般有以下几种:

1、Java方法栈帧中的局部变量(虚拟机栈的栈帧中的本地变量表,所引用的对象)
2、已加载类的静态变量(方法区中类静态属性,所引用的对象)
3、方法区中常量所引用的对象
4、JNI Handles(本地方法栈中JNI-Native方法,所引用的对象)
5、已启动且未停止的Java线程

1.2 处理GenRemSet

关于什么是GenRemSet我们先假设一种场景,在进行YGC时,如果新生代的的对象被老年代对象引用,那么称其为跨代引用。在YGC时,因为只回收新生代的对象,所以老年代的对象也是也可以算作一种特殊的GC ROOT,如果新生代的的对象被老年代对象引用,那么该新生代对象就应该判断为一个存活对象。

但是如果每次YGC的时候都得以老年代作为GC ROOT来扫描,况且YGC的频率那么高,性能方面肯定是难以接受的。

Hotspot就提供了GenRemSet这种数据结构解决这个问题,它呢主要用来记录对象对应的内存区域是否是dirty状态。当YGC进行GC ROOT扫描发现标记的存活对象在老年代且持有新生代对象的引用时,则会将该对象在GenRemSet映射的内存区域标记为dirty状态。

CardTable是GenRemSet的一种实现,类似于一个数组,每个元素对应着堆内存的一块区域是否存在跨代引用的对象,如果存在,该Card为dirty状态。
GenRemSet随着堆内存一起初始化,通过具体的垃圾收集策略进行创建,比如CMS和G1是不一样的,其中CMS对应的是CardTable,而G1则是RemberSet。

处理GenRemSet就是在GC ROOT遍历标记完之后就会开始处理GenRemSet,这时候就会扫描GenRemSet中的dirty card,将所有dirty card作为GC Roots,标记新生代的存活对象。

1.3 拷贝对象

因为新生代的结构是Eden+from survivor+to survivor结构的,一般而言,对象先在Eden中创建,然后做GC的时候将其复制(YGC基于复制算法)一份到to survivor,所以此时会拷贝所有存活对象到to区(若to区放不下则进行FGC)。

1.4 清除对象

最后就是清除未被标记(死亡)的对象,至此YGC流程结束。

二、老年代垃圾回收(CMS)

CMS的OGC使用标记-清除算法来回收死亡对象。

老年代垃圾回收有点特殊,因为细数JVM提供的GC垃圾收集器,也只有CMS GC实现了单独的老年代垃圾回收。
其中CMS触发老年代垃圾回收的条件由下面两个JVM启动参数控制:

-XX:CMSInitiatingOccupancyFraction=92
-XX:+UseCMSInitiatingOccupancyOnly

具体逻辑可参见我之前写的一篇文章:
CMS的CMSInitiatingOccupancyFraction解析

这里再简单介绍下CMS:
CMS收集器的主要设计目标是:降低应用停顿时间。它通过两种方式实现这一目标:
1、不压缩老年代,而是使用空闲列表来管理回收空间。
2、大部分标记清理工作与应用程序并发执行。

主要问题:由于标记-清除不压缩带来的老年代堆碎片,或者在对象分配率高的情况下,都可能导致Full GC。而在应用使用了大堆内存时,内存碎片引发的FGC会引发应用的长时间停顿,这个是比较严重的。所以CMS不适合大内存应用场景。

CMS收集器的OGC周期主要由7个阶段组成,其中有两个阶段会发生stop-the-world,其他阶段都是并发执行的。

2.1 Initial Mark(初始化标记)

初始化标记阶段,是CMS GC的第一个阶段,也是标记阶段的开始。主要工作是标记GC Roots可直达的存活对象 。

主要标记过程:
1、从GC Roots遍历可直达的老年代对象
2、遍历被新生代存活对象所引用的老年代对象

程序执行情况
1、支持单线程或并行标记。
2、发生stop-the-world,暂停所有应用线程。

2.2 Concurrent Mark(并发标记)

在该阶段, GC线程和应用线程将并发执行 。也就是说,在第一个阶段(Initial Mark)被暂停的应用线程将恢复运行。

并发标记阶段的主要工作是, 通过遍历第一个阶段(Initial Mark)标记出来的存活对象,继续递归遍历老年代,并标记可直接或间接到达的所有老年代存活对象 。

由于在并发标记阶段, 应用线程和GC线程是并发执行的,因此可能产生新的对象或对象关系发生变化 ,例如:
1、新生代的对象晋升到老年代
2、直接在老年代分配对象
3、老年代对象的引用关系发生变更

对于这些对象,需要重新标记以防止被遗漏。 为了提高重新标记的效率,本阶段会把这些发生变化的对象所在的Card标识为Dirty ,这样后续就只需要扫描这些Dirty Card的对象,从而避免扫描整个老年代。

2.3 Concurrent Preclean(并发预清理)

在并发预清洗阶段, 将会重新扫描前一个阶段标记的Dirty对象,并标记被Dirty对象直接或间接引用的对象,然后清除Dirty Card标识 。
1、标记被Dirty对象直接或间接引用的对象
2、清除Dirty对象的Card标识

2.4 Concurrent Abortable Preclean(可中止的并发预清理)

本阶段尽可能承担更多的并发预处理工作,从而 减轻在Final Remark阶段的stop-the-world 。
在该阶段,主要循环的做两件事:
1、处理 From 和 To 区的对象,标记可达的老年代对象
2、和上一个阶段一样,扫描处理Dirty Card中的对象

具体执行多久,取决于许多因素,满足其中一个条件将会中止运行:
1、执行循环次数达到了阈值
2、执行时间达到了阈值
3、新生代Eden区的内存使用率达到了阈值

2.5 Final Remark(重新标记)

预清理阶段也是并发执行的,并不一定是所有存活对象都会被标记,因为在并发标记的过程中对象及其引用关系还在不断变化中。

因此, 需要有一个stop-the-world的阶段来完成最后的标记工作 ,这就是重新标记阶段(CMS标记阶段的最后一个阶段)。 主要目的是重新扫描之前并发处理阶段的所有残留更新对象 。
主要工作:
1、遍历新生代对象,重新标记;(新生代会被分块,多线程扫描)
2、根据GC Roots,重新标记;
3、遍历老年代的Dirty Card,重新标记。这里的Dirty Card,大部分已经在Preclean阶段被处理过了。

2.6 Concurrent Sweep(并发清理)

并发清理阶段,主要工作是 清理所有未被标记的死亡对象,回收被占用的空间

2.7 Concurrent Reset(并发重置)

并发重置阶段,将 清理并恢复在CMS GC过程中的各种状态,重新初始化CMS相关数据结构 ,为下一个垃圾收集周期做好准备。

三、全堆的垃圾回收(SerialOld/ParallelOld)

说到FGC就不得不提serial old GC了,因为目前的GC垃圾收集器,像CMS/G1在发送FGC的时候实际是退化成serial old GC来做垃圾收集的,而像ParallelOld GC看似提供了自己的FGC实现,但它实际工作的代码和serial old GC是共用的一套代码。

serial old GC的Full GC使用标记-压缩(Mark-Compact)进行垃圾回收,它会在GC之后把所有存活对象滑动到空间的一端。大体流程如下:

3.1 根据GC ROOT标记存活对象

同YGC,主要都是先遍历找到所有的GC ROOT,然后再根据GC ROOT往下遍历,标记出所有存活的对象。

3.2 清除未被标记的对象

因为不像YGC只回收新生代对象以至于要把老年代对象也要作为特殊的GC ROOT来处理,这里FGC是回收整个堆的对象,所以扫描完GC ROOT查找存活对象的工作就完事儿了,然后就接着回收未被标记的对象。

3.3 计算对象新位置

因为FGC采用的是Mark-Compact(标记-整理)算法,回收完之后要将堆内存进行调整,解决内存碎片的问题,所以得将堆中的对象调整位置使其极可能整齐排列。所以这时候得先计算对象要移动到的新内存地址。

3.4 调整对象指针

计算好了对象新内存地址,就得将它的引用也修正过去。

3.5 移动对象

这时候万事俱备就直接移动对象到新内存地址就OK了,至此也是FGC流程走完。

总结

1、垃圾回收算法主要有三种,其中YGC使用的复制算法,OGC使用的标记-清除算法,FGC使用的标记-整理算法。

2、做单独的YGC或者OGC,都会存在跨代引用这个问题,YGC时老年代对象也得作为GC ROOT来查找存活对象,相应的OGC时新生代对象也得作为GC ROOT来查找存活对象。Hotspot使用GenRemSet避免了直接扫描整个新生代对象或老年代对象的尴尬情况,具体到CMS/G1的GenRemSet实现分别是CardTable/RSet。

3、能单独回收老年大的GC垃圾回收器只有CMS GC一个,受-XX:CMSInitiatingOccupancyFraction=92和-XX:+UseCMSInitiatingOccupancyOnly这两个JVM启动参数控制是否进行OGC。、

4、目前能提供FGC的GC垃圾回收器只有serial old GC和ParallelOld GC,但是ParallelScavenge GC的实现其实也是共用的serial old GC的逻辑。

若有错漏,敬请指出,万分感谢。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值