【读书会第12期】这可能是全网最大、最全的一张jvm收集器演化图和文字讲解了!

举报
breakDawn 发表于 2022/05/18 22:57:32 2022/05/18
【摘要】 假期借着华为云读书会的活动,重读了一遍《深入理解java虚拟机》, 发现第一遍读垃圾回收器相关的进化历程时,没有细细去研究各自的区别,觉得太多了记不住。 实际上理解了这个进化过程,这对于我们理解回收器是有很大帮助的。 **看来经典书籍要多读多总结,是有道理的。** 于是在阅读这个章节时,画了一张大的演化图,方便理解变化和区别。

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

看来经典书籍要多读多总结,是有道理的。
于是在阅读这个章节时,画了一张大的演化图,方便理解变化和区别。
欢迎关注一下我的华为云社区账号或者社区读书会活动。
欢迎点击该链接报名参加读书会,一起成长学习和交流!
报名链接


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

image.png

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

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

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

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

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

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

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的可能

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为了性能,会漏掉一些本该被回收的对象,但是无关大雅,大不了就下次再回收

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里也有?不是用了记忆集吗?不解,等以后有解答了,再来修改这里的内容。


最后的感想和完整回收器进化大图

好累,终于写完了,感觉能看到最后的人不会太多,但一通详细地分析和解决中间发现的问题,还是收获了不少。

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

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

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

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。