type
status
date
slug
summary
tags
category
icon
password
Property
Feb 5, 2025 08:21 AM
JVM垃圾回收技术总结
一、JVM垃圾判定
1.1 内存垃圾
JVM从物理内存中获取到可操作内存空间是有大小限制的,内存是有限的,怎么合理使用就需要定义一套规范体系,这也就是JMM。由于在程序执行过程中,链式函数调用产生的内存占用是一个重复的周期性规律性动作,因此,在JMM中,将一些普遍具有规律性的对象内存视为固有生命周期的内存对象,这些内存对象可以在生命周期结束后及时销毁,可通过链式函数重新生成,这个过程也就是内存释放回收的过程。
1.2 垃圾判定准则
JVM内部识别是否为垃圾,有两种官方定义的方式,其一,是根据在JVM所持有的内存中,Java对象对应的内存信息中,设有一个专属的被引用次数的信息,当被引用的时候,对引用关系数量进行计数,当引用数量归0的时候,视为垃圾对象,这就是引用计数法,优点是方法简单,但是缺点也很显而易见,如果A引用B,B引用A,形成循环引用,引用计数法就卡了Bug,无法处理这种关系;其二,是根据在JVM所持有内存中,通过从一系列称为GCRoots的对象出发,判断这些对象是否持有对其他对象的内存地址,也就是视为对其他对象的引用,然后继续排查,这个引用的链路就是引用链,当一个对象到GCRoots没有任何引用链相连时,则证明此对象是不可用的,被判定为垃圾对象。
1.3 GC-Root判定
GCRoot的判定根据官方文档GC-Root的判定遵循以下原则:
- Java栈中的局部变量和方法参数:
- 在方法的执行过程中,局部变量和方法参数所引用的对象被视为GC Roots。
- 活动线程:
- 活动线程本身及其引用的对象被视为GC Roots。
- 类的静态变量:
- 类的静态变量所引用的对象被视为GC Roots。
- JNI引用:
- 通过JNI(Java Native Interface)注册的外部引用的对象被视为GC Roots。
- 运行时常量池中的对象引用:
- 在方法区的运行时常量池中引用的对象被视为GC Roots。
- 同步锁集合中的对象:
- 持有锁的对象被视为GC Roots。
二、JVM垃圾回收
垃圾回收离不开收集器,垃圾回收线程的启动是由字节码执行引擎触发的。虽然垃圾回收器本身是独立模块,但其触发逻辑(如内存分配失败、主动调用GC方法等)需要通过执行引擎来发起。当满足垃圾回收条件时,执行引擎会向垃圾回收器发送信号,由后者创建并管理专用的垃圾回收线程进行内存清理。
2.1 垃圾回收器
- 第一代垃圾回收器:Serial垃圾收集器 Serial垃圾回收器是第一代垃圾回收器,Serial收集器支持年轻代与老年代区域的内存回收。并且,Serial收集器针对不同代的垃圾采用不同的回收算法,对于新生代,Serial垃圾收集器采用复制算法,对于老年代采用标记-整理算法。并且,针对于老年代,Serial系列还有专门的老年代垃圾回收器,Serial Old回收器,它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。值得注意的是,Serial收集器他是一个串行收集器,也就是单线程收集器,在执行垃圾回收的过程中会强制STW(停止掉所有用户线程专门负责垃圾收集),他的主要问题是:效率太低。如图所示:

- 第二代垃圾回收器:Parallel Scavenge收集器 Parallel收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器类似。默认的收集线程数跟cpu核数相同,当然也可以用参数(-XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改。与Serial最大的区别就在他是多线程收集,但同样也会STW。Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。同样,与Serial收集器相同的是,Parallel Scavenge收集器对新生代采用的收集算法仍然是复制算法,对于老年代采用的算法仍然是标记-整理算法。并且与Serial收集器一样的是,针对于老年代,Parallel Scavenge收集器提供了专门的Parallel Old收集器,此收集器使用多线程和“标记-整理”算法。JDK8默认的垃圾回收器就是,Parallel Scavenge(新生代)+ Parallel Old(老年代)组合的垃圾回收器。STW时间会比较长(如果内存比较大的话)。

- 第三代垃圾回收器:ParNew收集器 ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用。ParNew是专用于新生代垃圾回收的收集器,新生代采用复制算法。

- 第三代垃圾回收器:CMS垃圾收集器 CMS是专注于回收老年代的垃圾回收器,CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
- 初始标记: 暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快。
- 并发标记: 并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线 程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录(主要是处理漏标问题),这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法做重新标记。区别于G1,G1是通过原始快照做重新标记。
- 并发清理: 开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理(见下面三色标记算法详解)。
- 并发重置:重置本次GC过程中的标记数据。

- 第四代垃圾回收器:G1收集器 G1是分区回收,而CMS主要是分代回收。G1将JVM所持有的内存分为一个一个的小的Region。并且,G1 可以预测停顿时间,并根据应用的需求进行调整,以达到预期的停顿时间目标。G1 使用复制算法,可以有效地避免内存碎片问题。G1 可以根据应用的实际情况动态调整回收策略,提高性能。正常来说, 年轻代满了之后就会进行YoungGC但是G1不一定会这样,因为如果YoungGC满了回收时间达不到200ms,比如20ms,不会GC,继续增加Edan区域,直到估算时间接近200ms,触发YoungGC。
- 第五代垃圾回收器:ZGC收集器
未完待续…
2.2 CMS回收细节之一
字节码执行引擎启动的垃圾回收线程,注意,垃圾回收工作线程与用户线程不是一个线程。JVM的垃圾回收线程是典型的守护线程。它持续监控内存使用情况,自动回收无用对象,无需开发者手动管理,而业务线程是主要产生垃圾的线程。在垃圾回收的过程中,我们重点关注CMS的垃圾回收细节,CMS垃圾回收主要分为五个阶段,其中在初始化标记阶段的时候,只会标记GC-ROOT直接引用的对象,并且会进行STW,之后会进入下一阶段,也就是并发标记阶段,此时为了用户体验,不会进行STW,而是与用户线程并行,这也是与Parallel收集器最大的差别所在,此过程中回收线程会继续沿着GCRoot继续标记,而正是因为与用户线程一起进行,所以这里面会有很多问题,比如一个对象最开始标记为垃圾,但是并发标记过程结束后不是垃圾了,再比如一个对象最开始不是垃圾,但是标记流程结束后它又是垃圾了,为了解决这个问题,CMS会在下一个阶段,也就是重新标记阶段,基于三色标记法,进行确认,此阶段也会STW,用于修复并发标记的问题。此阶段不用垃圾回收器在修复问题的策略上各有不同的解决方案:
2.3 CMS回收细节之二
关于三色标记
- 白色: 表示对象尚未被垃圾收集器访问过。
- 灰色: 表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。
- 黑色: 表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。
三色标记普遍应用于并发标记场景,如CMS垃圾收集器中,由于支持并发标记,垃圾回收线程与用户业务线程同时进行,并发标记阶段可能会出现漏标的场景,而不同的垃圾收集器,在相同的三色标记算法上,细节略有不同,CMS基于三色标记,在处理并发标记漏标问题的时候,采用的策略是增量更新(Incremental Update),而G1在处理三色标记漏标问题的时候,采取的策略则是原始快照(SATB,Snapshot At The Beginning)。三色标记在垃圾回收的过程中,从初始标记开始,贯穿整个垃圾回收,它有着不可估量的作用。
增量更新
- 当黑色对象通过用户线程新增对白色对象的引用时(例如黑对象A插入指向白对象D的引用),CMS通过写屏障(Write Barrier)记录这一变化,并将黑对象A降级为灰色对象。在后续的重新标记阶段(STW),需从灰色对象A开始重新扫描其引用链,确保白对象D被正确标记。本质:破坏漏标的第一个条件(黑对象不再直接引用白对象),但需额外扫描新增引用的路径,可能导致重新标记阶段耗时较长。
原始快照
- 在标记开始时对对象引用关系建立快照。若用户线程删除了灰色对象到白色对象的引用(例如灰对象B取消指向白对象D的引用),G1通过写屏障将原本被删除的引用记录到队列中。在标记完成后,会补充扫描这些记录的引用链,确保白对象D不被漏标。本质:破坏漏标的第二个条件(删除灰对象到白对象的引用),即使引用被删除,仍按快照的原始引用链处理,避免漏标,但可能产生浮动垃圾。
2.4 重要的概念
(1)浮动垃圾
标记整理的效果可以在CMS中开启参数支持,CMS本身清理过程是标记清除,会产生内存碎片,如果想要收集垃圾的时候整理内存碎片,那么需要开启次参数;(CMS是支持标记清除后整理的)

CMS有一个参数需要注意:
-XX:CMSFu1lGCsBeforeCompaction=0与等于1,虽然在大多数情况下,结果导向是一样的,那就是在做完FullGC之后,执行一次内存碎片整理,
(2)CMS的高一致性
当并发标记过程中出现失败或无法继续进行时,为了保证系统的稳定性和一致性,JVM可能会选择取消并发执行,并转而采用完全的停止-工作(Stop-The-World, STW)方式完成垃圾回收。这种情况下,所有的用户线程都会被暂停,垃圾回收器会一次性扫描整个堆内存,完成对象的标记和清理工作。这种方式虽然会带来较长的停顿时间,但可以确保垃圾回收过程的正确性和一致性

(3)跨代垃圾回收
年轻代与老年代之间的垃圾回收联系主要通过“卡表(Card Table)”和“记忆集(Remembered Set,简称RS)”来实现。这两种机制是JVM在处理跨代引用问题时的关键技术,用于优化垃圾回收效率并减少不必要的内存扫描开销。
卡表是一种用于记录老年代中哪些区域可能包含指向新生代对象的引用的数据结构。这种数据结构的核心思想是将老年代划分为固定大小的内存块(称为“卡页”或“Card”),每个卡页对应一个位图(Bitmap)。当老年代中的对象引用了新生代的对象时,对应的卡页会被标记为“脏”(Dirty)。在进行年轻代垃圾回收时,只需要扫描这些标记为“脏”的卡页,而无需遍历整个老年代,从而大幅降低扫描开销。
记忆集是一种抽象数据结构,用于记录从非收集区域(如老年代)指向收集区域(如新生代)的引用关系。其目的是避免在年轻代垃圾回收时扫描整个老年代,从而提高效率。很明显,记忆集的概念比卡表更加抽象。
- 作者:fntp
- 链接:https://polofox.com/article/jvm-jmm-4
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章