62、垃圾回收器(下)
上一节我们讲到,STW 停顿时间是垃圾回收器非常关注的一个性能指标
为了尽量减少 STW 停顿时间,CMS 和 G1 垃圾回收器支持并发垃圾回收,也就是在不完全暂停应用程序的同时,并发执行垃圾回收工作
那么并发垃圾回收是如何实现的呢?本节我们就来讲一讲:并发垃圾回收、三色标记算法
1、并发垃圾回收
需要注意的是,这里所说的并发垃圾回收,并非完全不需要暂停应用程序,而是在大部分时间里都不需要暂停应用程序
并发垃圾回收将垃圾回收的整个过程分为 4 个阶段:初始标记、并发标记、重新标记、并发清理
- 初始化标记和重新标记这两个阶段,仍然需要暂停应用用程序的执行
- 并发标记和并发清理这两个比较耗时的阶段,可以与应用程序并发执行
初始标记指的是标记 GC Roots,并发标记指的是在应用程序不暂停的情况下,以 GC Roots 为起点,广度优先或深度优先遍历所有可达对象(也即存活对象)
在并发标记的过程中,应用程序有可能修改对象之间的引用关系,导致并行标记过程出现误标或漏标的情况,重新标记所做的工作就是对误标或漏标进行修正
并发清理指的是在不暂停应用程序的情况下,对标记出来的垃圾对象进行清理
在以上 4 个阶段中,初始标记、并发标记、重新标记这三个阶段属于可达性分析,也就是标记 - 清除算法中的标记环节,并发清理是标记 - 清除算法中的清除环节
接下来我们就来详细讲解一下这 4 个阶段是如何实现的,用于可达性分析的三色标记算法是讲解的重点
2、三色标记算法
在进行可达性分析时,三色标记算法将遍历过程中的对象分别标记为:白色、灰色、黑色三种类型,各个颜色的含义如下所示
- 白色:对象没有被遍历过,在遍历开始时,所有的对象都是白色,但当遍历结束后,如果对象仍为白色,那么就表示对象不可达
- 灰色:对象已经被遍历,但是对象所直接引用的对象还没有完全被遍历
- 黑色:对象已经被遍历,并且对象所直接引用的对象都已经被遍历
前面讲到,可达性分析是基于图的广度或深度优先遍历算法来实现,我们拿广度优先遍历算法来举例讲解,看下遍历的过程中,对象的颜色是如何标记和转换的
- 初始化将 GC Roots 标记为灰色,并放入灰色集合,剩余的其他节点标记为白色,并放入白色集合
- 从灰色集合中取一个灰色对象,标记为黑色,放入黑色集合,并将此对象直接引用的所有白色对象标记为灰色,并放入灰色集合
- 重复上述第 2 步,直到灰色集合中没有对象为止
此时,黑色集合中存放的就是可达对象,也就是存活对象,白色集合中存放的就是不可达对象,也就是死亡对象
在上述的处理过程中,步骤 1 被称为初始标记阶段,步骤 2 和 3 为遍历过程,在并发垃圾回收中,可以与应用程序并发执行,因此被称为并发标记阶段
我们通过一个例子来解释一下上述处理过程,具体如下图所示
3、误标和漏标
在上述三色标记算法的执行过程中,并发标记不会暂停应用程序的执行,应用程序在执行的过程中,有可能会改变对象的引用关系,从而导致存活对象的误标或者漏标问题
- 误标:将非存活对象误标为存活对象
- 漏标:将存活对象漏标,漏标的存活对象会被判定为死亡对象
接下来,我们来看下误标和漏标是如何产生的
3.1、误标
如果当可达性分析进行到如下图所示的状态时,应用程序执行了 "objA.fieldB = null;" 这样一条语句(注意:objA 为堆外的局部变量或静态变量,fieldB 为对象的成员变量)
因为对象 B 已经被标记为灰色,并且存储在灰色集合中,所以 B 及其 B 所引用的对象(C、D)仍然会被判定为存活对象,从而产生了误标问题
3.2、漏标
如果当可达性分析进行到如上图所示的状态时,应用程序执行了如下两行代码,此时又会出现什么问题呢?
这里需要注意一下,objA 为堆外的局部变量或者静态变量
除此之外,因为对象 B、C 不为 GC Roots,所以对象 B、C 没有被堆外的局部变量或静态变量所直接引用
如果要访问这两个对象,那么我们需要像如下两行代码这样,从 objA 变量一路追溯过去
objA.fieldC = objA.fieldB.filedC; // 新增引用 objA.filedB.filedC = null; // 删除引用
应用程序执行上述代码,会导致 B 到 C 之间的引用关系断开,A 到 C 之间的引用关系建立,如下图所示
因为 A 是黑色对象,不会再被遍历,所以尽管 C 是存活对象,但不会被遍历到,从而出现漏标的问题
4、增量更新和原始快照
并发标记的误标和漏标问题,会在重新标记中解决
- 误标问题不大,是可以接受的,其只会导致垃圾对象延迟回收,本该在本轮回收的对象在下一轮垃圾回收中回收
- 漏标问题很大,不可以接受,本不该被回收的对象被回收,这就会导致应用程序的运行出错,因此漏标是重新标记要解决的重点问题
我们总结一下漏标产生的原因,主要有以下两点,两者缺一不可
- 新增引用:新增一个黑色对象对一个白色对象的引用
- 删除引用:删除所有灰色对象到此白色对象的直接或间接的引用
针对以上两点,Java 发明了两种漏标解决方案
- CMS 垃圾回收器使用 "增量更新" 解决漏标问题
- G1 垃圾回收器使用 "原始快照" 解决漏标问题
实际上增量更新和原始快照这两种解决方案的处理思路是类似的,只不过增量更新记录的是新增的引用关系,原始快照记录的是删除的引用关系
接下来我们简单介绍一下这两种解决方案
4.1、增量更新
在并发标记的过程中,如果应用程序:新增了一个黑色对象对一个白色对象的引用,那么虚拟机将这个白色对象记录下来
在并发标记完成之后,重新标记阶段会以这些记录下的白色对象为起点,重新进行可达性分析,这样漏标的白色对象会被重新标记为黑色对象
4.2、原始快照
在并发标记的过程中,如果应用程序:删除了一个灰色对象对一个白色对象的直接引用或间接引用,那么虚拟机会将这个白色对象记录下来
在并发标记完成之后,重新标记阶段会以这些记录下来的白色对象为起点,重新进行可达性分析
这就相当于虚拟机对引用关系改变之前的原始快照进行可达性分析
不过原始快照记录下的白色对象有可能是死亡对象,而重新标记阶段会将这些死亡对象重新标记为存活对象,因此原始快照这种解决方案会导致误标问题
不过前面讲到,相对于漏标问题,误标问题不大,是可以接受的
5、并发清理和新建对象
并发垃圾回收包括 4 个阶段:初始标记、并发标记、重新标记、并发清理
前面我们讲了前 3 个阶段,它们隶属于可达性分析,基于三色标记算法来实现
接下来我们再来看下最后一个阶段:并发清理,为什么并发清理阶段可以跟应用程序并发执行?
在并发清理阶段,对象的引用关系也有可能发生改变
存活对象有可能会变为死亡对象,但这些对象只需要在下一次垃圾回收中被回收即可
反过来,死亡对象是否有可能会变为存活对象,从而导致清理操作将存活对象清理掉呢?
答案是不会,死亡对象不再有变量(局部变量或静态变量)的直接或间接引用,因此应用程序是无法在代码中使用这些死亡对象,如下代码所示
函数内创建的对象,在函数内部可以使用,这是因为有局部变量的存在,我们可以通过局部变量来访问这个对象
但是在函数执行结束之后,局部变量被销毁,没有变量直接或间接的引用这个对象,我们也就无法在代码中访问这个对象了
public void f () { Student stu = new Student(); stu.age = 19; stu.name = "wangzheng"; }
有些读者可能会说,如果在并发清理的过程中,应用程序创建了新的对象该怎么办呢?
解决方法很简单:直接将新创建的对象标记为黑色,即存活对象,这种处理方案同样适用于并发标记阶段,当然这也有可能会导致误标问题,至于为什么,留给读者来思考
6、课后思考题
为什么 CMS 和 G1 都使用标记 - 清除,而非标记 - 整理算法来回收老年代的垃圾对象?
垃圾回收时间快,减少 STW 时间,在多次垃圾回收之后进行一次碎片整理有效避免了分配效率问题和空间利用率问题
本文来自博客园,作者:lidongdongdong~,转载请注明原文链接:https://www.cnblogs.com/lidong422339/p/17498615.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步