0%

jvm垃圾收集器演进和原理详解

重读了一遍《深入理解java虚拟机》, 发现第一遍读垃圾回收器相关的进化历程时,没有细细去研究各自的区别,觉得太多了记不住。
实际上理解了这个进化过程,这对于我们理解回收器是有很大帮助的。

看来经典书籍要多读多总结,是有道理的。
于是在阅读这个章节时,画了一张大的演化图,方便理解变化和区别。

[toc]


Serial Old和Serial(单线程收集器)#

image.png

  • Serial Old指的是老年代收集器,使用标记-整理清理垃圾
  • Serial指的是年轻代收集器,使用复制算法清理垃圾。

Serial就是单线程的意思,不仅代表它使用单线程做回收,更意味着他会进行stop world暂停工作线程

Serial类型的收集器被淘汰了吗?它还有优势吗?#

没有,它是client模式下默认的收集器。
优势在于,它具有最高的单线程收集效率
而client模式一般不会用于处理大量请求,因此非常适合serial。

除了client,书上还提了另外2个功能:

  • Serial Old收集器会作为CMS的后备预案
  • 与Parallel Scavenge搭配使用

Q: 什么时候可以触发stopWorld?(安全点,GC时间点)#

  • 方法返回之前
  • 调用某个方法之后
  • 抛出异常的位置
  • 循环的末尾

4个安全点的设计思路是什么?#

当垃圾收集需要中断用户线程的时候, 不直接对线程操作,而是设置一个标志位.

各个用户线程执行过程中会轮询这个标志位的状态.

一旦用户线程轮询到标志位被设置过, 就在最近的安全点主动挂起,所有用户线程都挂起, 垃圾收集真正进行

Q: 和安全点对应的安全区又是什么?#

A:
程序可能因为blocked或者sleep,无法到达安全点。
这是可以设置一个安全区域,这个安全区域对应的代码段,引用关系不会变。JVM检测到程序在安全区域时,可以进行GC。
程序运行出安全区域时,检测GC没有结束的话,自我中断。

ParOld和ParNew收集器(多线程收集器)#

image.png
Par收集器就是Serial收集器的多线程版本,其他策略则都与serial一致。

注意, 虽然是多线程收集器,但是用户的工作线程仍然是暂停状态(为了防止收集过程中发生变化导致回收错误

ParNew收集器可以与CMS收集器配合使用。

Parallel Scavenge收集器(对回收时间的优化开端)#

image.png

Parallel Scavenge 收集器是一个新生代收集器,他不包含老年代。
前两代的收集器,默认必须收集完成,对工作线程影响巨大。
这是首次开始关注回收时间对工作线程影响的一代收集器,成为了垃圾收集器升级优化的一个重要开端。

如何理解吞吐量#

虚拟机运行了100分钟, 垃圾收集花掉了一分钟,那么吞吐量就是100%。

Parallel Savenge收集器是如何控制吞吐量的#

通过控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数,以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
通过修改这2个参数,jvm可以计算一个合适的新生代空间,空间越小,回收时间越快,他的停顿时间便能够满足吞吐量要求。

  • 换句话说,本质上就是通过你设定的吞吐量或者暂停时间,自适应地得到一个新生代空间大小而已。

代价是什么?#

新生代越小,那么意味着老年代的空间就越大。

虽然能做到基本不停顿或者停顿间隔很小,但这样就会导致新生代频繁发生minorGc,并不断将垃圾扔给老年代收集器,容易在下一个时间段触发更多的fullGc。
因此这个策略仅仅是饮鸠止渴,无法真正解决问题。

  • 注意,这里ParaleelSavenge的吞吐量,指的就是新生代的吞吐量,不代表fullGc占用的时间。

CMS收集器(Concurrent Mark Sweep, 并发收集概念的重大提出)#

image.png
而从这一代开始,jvm终于想到了可以如何尽可能少地暂停工作线程的方式,提出了并发收集的概念。

首先明确并行收集和并发收集的区别

  • 并行收集:指用多线程来收集,但是工作线程仍然暂停
  • 并发收集:收集线程和工作线程允许不冲突地交替并发执行

CMS是老年代收集器,必须和parNew等结合使用
使用的是回收-清除算法,有较多碎片。


CMS阶段1:初始标记#

初始标记就是对GCRoot对象进行标记,以GCroot作为起点。
GCRoot的那4种经典对象,瞄一眼就好

虚拟机栈(栈帧中的本地变量表)中的引用的对象
方法区中的类静态属性引用的对象
方法区中的常量引用的对象
本地方法栈中JNI(Native方法)的引用对象

GCRoot的选取原则是什么?#

这个问题很有意思,为什么要这样选择?如果能理解这个问题,也就不需要去死记硬背上面的内容了。

首先,可能有很多个方法栈,每个栈都有一个栈顶的栈帧,说明这是正在执行的方法, 在此刻是一定不需要被回收的!
因此选取了2种栈的栈顶作为GCRoot选取位置。

而方法区中的对象一般不会被释放,长期持有,因此方法区中的静态引用对象、常量引用对象也是稳定能被使用的。

ParNew年轻代收集时,需要遍历CMS老年代的所有GCROOT吗?#

CMS是老年代的收集器, 经常要和年轻代收集器例如ParDoNew配合。(因此上面4个阶段都是处理老年代回收的,年轻代内存占用小,不需要那么麻烦)
那么当ParNew年轻代回收时,是否也要把老年代的所有GCROOT都算上?后面全部遍历的话,时间是不是太久了?

因此才有了卡表的出现!
卡表作为一个比特位的集合,每一个比特位可以用来表示年老代的某一区域中的所有对象是否持有新生代对象的引用。
这样新生代在GC时,可以先扫描卡表,只有卡表的标记位为1时,才需要扫描给定区域的年老代对象。而卡表位为0的所在区域的年老代对象,一定不包含有对新生代的引用,从而提高了年轻代的回收效率!

CMS阶段2:并发标记#

这时候不会做stopWorld。标记线程和工作线程同时进行。

并发标记用了怎样的算法去标记的?#

当通过gcRoot做并发标记的时候,是一种bfs搜索。
有一种三色标记法可以作为参考:

  • 白色:还没有搜索过的对象(白色对象会被当成垃圾对象)
  • 灰色:正在搜索的对象
  • 黑色:搜索完成的对象(不会当成垃圾对象,不会被GC)
  1. 默认起始是白色节点。
  2. 是每次标记当前搜索节点的引用节点(类似于相邻点)为灰色,入队列。
  3. 当相邻点全部入队列完成,则把当前搜索节点置黑色。然后根据队列取队头继续处理灰色节点

是不是和数据结构的bfs非常类似?

并发标记时如何记录引用变更?#

对于CMS在并发标记时的引用变更,书上没有细讲,只是一笔带过,个人认为错失了许多精华。
有些类似的概念确实有在G1收集器里简单阐述,但是很难让人马上和CMS中对应起来。个人认为应该在CMS的章节就提前给出。
下面以我自己的理解,给出对CMS并发标记过程的理解。

什么是跨代引用?#

首先基于上面提到的三色标记,给出跨代引用问题的例子和解释。
假设此时正处于并发标记中,且正好在bfs处理A这个节点。
image.png
这时取消了A对B的引用,以及B对C的引用,同时新增了A对C的引用,变成如图所示:
image.png
那么当继续搜索入队的B时,将无法再走到C,C永远被标记为白色,就会出现严重的后果:误杀了C,从而导致A对C调用时报错!
image.png

如何避免跨代引用,保障并发标记安全(写屏障#

CMS引入了一个叫“写屏障”的东西,写屏障工作示意如下:
image.png
而标记栈就会在后面的“重新标记”阶段用上。

CMS阶段3:重新标记#

前面的写屏障为我们把为标记却被新增引用的对象放入了栈中。
此时会进入StopWorld,我们可以从栈中取出标记对象进行“重新标记”了。
image.png

CMS阶段4:并发清除#

最后清除的时候,选用了“标记-清除”算法,来进行回收和处理。
同时采用并发机制,避免影响了工作线程。
“标记-清除”算法的示意图如下:
image.png

为什么是要用“标记-清除”这么缺点大的方法?#

因为老年代算法,要么是标记-整理,要么是标记-清除
而标记-整理算法是无法和工作线程并发执行的
所以才选择标记-清除,这也导致了碎片带来的隐患

CMS如何解决标记-清除后碎片过多,无法放入新对象的情况?#

当因为碎片过多,无法放入新对象时,会触发fullGC,此时会做1次内存碎片的合并(整理)操作

还提供了一个参数,设置多少次非合并的fullGC时,可做一次碎片的集中合并和整理。

并发回收过程中,如果工作线程突然生成大量新垃圾,导致内存不足怎么办?#

因为并发回收时工作线程还在运行,可能产生大量的对象,导致老年代被填满。
这时候CMS会触发一个“Concurrent Mod Failure”机制,并紧急替换为SerialOld收集进行stopWorld回收。
因此,CMS可能存在临时退化为SerialOld的可能

混合使用收集器(CMS+parNew)#

G1收集器出现之前, 都是老年代和年轻代一起使用, 例如CMS+parNew的组合

Q: fullGC和monorGC分别发生在老年代区还是新生代区?#

A:

fullGC只会发生在老年代区。 会用上CMS那堆很复杂的操作

相反,minorGC一般发送在新生代区,直接用parNew那种复制算法,很快。

Q: 新生对象什么时候可以尝试进入老年代?#

  1. 发生minorGC后, E区和survivor区都放不下了

  2. 在survivor区待的时间过长,gc分代年龄超过阈值

  3. 该GC分代年龄对象总和大于等于s区的一半

  4. 新生对象的大小大于某个阈值

Q: Eden年轻代区和survivor区的比例为什么是8比1比1#

A:

正常预留10%内存空间, 大于90%进行minorGC
但为了复制算法需要, 得有这10%的复制

且有-XX:SurvivorRatio=8 这个参数值可以修改

Q: 讲一下老年代的担保策略是做什么的?#

用于确认此时到底是做minorGC还是做fullGc。

  1. 在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间
  2. 如果这个条件成立,那么Minor GC可以确保是安全的。
  3. 如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。
  4. 如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小
  5. 如果大于平均大小,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的,有可能把老年代搞满(这时候还没有fullGC!);
  6. 如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC即全量GC,而不是minor小gc。

换言之, 要么选择minorGc,要么选择fullGc, 感觉有风险时,就fullGc。

新生代、老年代的交互流程:#

1660746972644

G1收集器(回收概念发生诸多变革,目前最先进收集器)#

image.png

G1收集器,书上感觉并没有讲得特别深,很多概念、区别都没讲好,个人认为是种遗憾,因此我在透彻学习G1之前,我也只能简单写写了。

G1相比CMS的重大升级点:#

  1. 回收范围不同。 CMS是老年代收集器,必须和parNew等结合使用。 G1则可以同时管理老年代和年轻代。
  2. 停顿目标不同。 CMS会让停顿时间尽可能小, G1则建立了可预测的时间模型。
  3. 清理方式不同。 CMS是标记-清除, G1是标记-整理,碎片大大减少。
  4. G1支持筛选回收 G1可以根据每个region的价值进行回收,CMS则不行。
  5. 并发标记后的最终标记处理方式不同
    这个标记方式的区别讲述起来有点抽象,简而言之就是:
  • CMS是希望记录所有新增的引用,并重新做好多次BFS,保证没有疏漏,代价非常大。
  • G1则是只更新o = null这种删除引用的情况。对于新增的引用,直接认为那个对象不需要杀。
    换句话说,CMS更严谨,做细致的重新检查。 而G1为了性能,会漏掉一些本该被回收的对象,但是无关大雅,大不了就下次再回收

G1里的region之间都要通过BFS遍历吗?#

这个问题,和之前CMS中回收年轻代时, 是否要走一遍全量的老年代是一个道理。

G1里用的是一个RememberSet来避免全region扫描的。
每个G1的region都有一个记忆集(Rset)
记忆集会记录下当前这个region中的对象被哪些对象所引用。
例如,region2中的两个对象分别被region1中的对象和region3中的对象所引用,那么,region2的记忆集记录的就是region1和region3中的引用region2的对象的引用。

这样一来在回收region2的时候,就不用扫描全部的region了,只需要访问记忆集,就知道当前region2里面的对象被哪些对象所引用,判断其是不是存活对象。

简单来说,就是标记我这个region被哪些region引用,简化扫描,避免不必要的检索。


但是书上提到了一句话(P85):

“通过cardTable卡表把相关引用信息记录到被引用对象所属的region的rememberedSet之中”

这里我就点没看懂,卡表不是老年代对年轻代的引用么,为什么G1里也有?不是用了记忆集吗?不解,等以后有解答了,再来修改这里的内容。


Q: G1里的minorGc、 majorGc、mixedGc、fullGc的区别?

A:

  • minorGc也就是youngGc。工作流程很简单:线程跑,然后就进行青年代Region的回收,把需要回收的YoungRegion,放入YoungCSet中,在YGC阶段就进行对年轻代CSet中的Region进行回收。因为大部分都是垃圾,且用了复制回收算法,基本只需要较短时间的STW就能完全回收了。 这里不会触发
  • MixedGc等于年轻代+部分老年代gc。当老年代垃圾达到一个阀值的时候,会触发,这里就会用到G1里的并发标记、最终标记、筛选回收等操作。但不会针对所有老年代,只筛选需要处理的老年代。
  • FullGc,等于整个堆空间的清理。 当新对象进入老年代时空间不足, G1便会触发担保机制,执行一次STW(stopWorld)式的、单线程的Full GC。Full GC会对整堆做标记清除和压缩,最后将只包含纯粹的存活对象。 注意这时候不会用到并发之类的,直接暂停了。

完整大图#

关于垃圾收集,书上倾向于先将一些基本概念或者基本回收思路,再讲发展流程,同时对G1缺少更细致的解释,这就容易混杂起来,导致垃圾收集器的进化那一章节看得很迷。

后面找到了一本书,叫做《The Garbage Collection Handbook》,已经收藏,有时间的话可以看看,据说对G1做了非常细致的讲解
image.png

最后送上完整大图:
垃圾收集器大图

其他问题#

Q: 方法区里的class对象(即类对象)什么时候会被回收?#

A: 所有实例都被回收、对应classLoader也被回收、class对象不会再被引用或者反射(这个咋确定?当初书里看到的,没懂)

Q: 什么时候会调用对象的finalized方法#

A: JVM启动垃圾回收,且该对象要被回收时。

finalized应该更多是规范吧,很多规范里都要求我们不要自己实现finalized了,毕竟不确定性太大。