JVM垃圾回收详解
堆空间的基本结构
Java 的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是堆内存中对象的分配与回收。
Java 堆是垃圾收集器管理的主要区域,因此也被称作 GC 堆(Garbage Collected Heap)。
从垃圾回收的角度来说,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆被划分为了几个不同的区域,这样我们就可以根据各个区域的特点选择合适的垃圾收集算法。
JDK堆内存被通常分为下面三部分:
- 新生代(Young Generation)
- 老年代(Old Generation)
- 元空间(Metaspace)
下图所示的 Eden 区、两个 Survivor 区 S0 和 S1 都属于新生代,中间一层属于老年代,最下面一层属于永久代。

JVM 一次完整的 GC 流程是什么
- 首先,任何新对象都分配到
eden空间。两个幸存者空间开始时都是空的。 - 当 eden 空间填满时,将触发一个
Minor GC(年轻代的垃圾回收,也称为 Young GC),删除所有未引用的对象;大对象(需要大量连续内存空间的 Java 对象,如那种很长的字符串)直接进入老年代。 - 所有被引用的对象作为存活对象,将移动到第一个幸存者空间 S0,并标记年龄为 1,即经历过一次 Minor GC。之后每经过一次 Minor GC,年龄 + 1。GC 分代年龄存储在对象头的 Mark Word 里。
- 当 eden 空间再次被填满时,会执行第二次 Minor GC,将 Eden 和 S0 区中所有垃圾对象清除,并将存活对象复制到 S1 并年龄加 1,此时 S0变为空。
- 如此反复在 S0 和 S1 之间切换几次之后,还存活的年龄等于 15 的对象(JDK8 默认 15,JDK9 默认 7,-XX:InitialTenuringThreshold=7)在下一次 Minor GC 时将放到老年代中。
- 当老年代满了时会触发 Major GC(也称为 Full GC),Major GC 清理整个堆:包括年轻代和老年代。
JVM 垃圾回收机制
- 新生代收集(Minor GC / Young GC):只对新生代进行垃圾收集;
- 老年代收集(Major GC / Old GC):只对老年代进行垃圾收集。需要注意的是 Major GC 在有的语境中也用于指代整堆收集;
- 混合收集(Mixed GC):对整个新生代和部分老年代进行垃圾收集。
- 整堆收集 (Full GC):收集整个 Java 堆和方法区。
[!note] Full GC 详解
Full GC(Full Garbage Collection)是指对整个堆内存进行垃圾回收的过程。在进行 Full GC 时,会对年轻代和老年代(以及永久代或元数据区)中的所有对象进行回收。
Full GC 通常发生在以下情况之一:
- 显式触发:通过调用 System.gc() 方法显式触发垃圾回收。虽然调用该方法只是向 JVM 发出建议,但在某些情况下,JVM 可能会选择执行 Full GC。
- 老年代空间不足:当老年代空间不足时,无法进行对象的分配,会触发 Full GC。此时,Full GC 的目标是回收老年代中的无效对象,以释放空间供新的对象分配。
- 永久代或元数据区空间不足:在使用永久代(Java 8 之前)或元数据区(Java 8 及之后)存储类的元数据信息时,如果空间不足,会触发 Full GC。
Full GC 是一种较为耗时的操作,因为它需要扫描和回收整个堆内存。在 Full GC 过程中,应用程序的执行通常会暂停,这可能会导致较长的停顿时间(长时间的停顿会影响应用程序的响应性能)。 为了避免频繁的 Full GC,通常采取一些优化措施,如合理设置堆大小、调优垃圾回收参数、减少对象的创建和存活时间等。
对象死亡判断方法
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断哪些对象已经死亡(即不能再被任何途径使用的对象)。
引用计数法
给对象中添加一个引用计数器:
- 每当有一个地方引用它,计数器就加 1;
- 当引用失效,计数器就减 1;
- 任何时候计数器为 0 的对象就是不可能再被使用的。
这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间循环引用的问题。

所谓对象之间的相互引用问题,如下面代码所示:除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们。
1 | public class ReferenceCountingGc { |
可达性分析法
这个算法的基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。

哪些对象可以作为 GC Roots 呢?
- 虚拟机栈(栈帧中的局部变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
- JNI(Java Native Interface)引用的对象
对象可以被回收,就代表一定会被回收吗?
即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。
被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
引用类型总结
JVM 中主要有四种引用类型:强引用、软引用、弱引用、虚引用
强引用(Strong Reference)
- 概念: 强引用是最常见的引用类型。如果一个对象具有强引用,那么垃圾收集器绝不会回收它。当内存空间不足时,JVM 宁愿抛出
OutOfMemoryError,也不会回收强引用的对象。 - 创建方式: 通常使用
new关键字创建对象时就是强引用。 - 应用场景: 只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
软引用(Soft Reference)
- 概念: 软引用比强引用弱一些。在内存充足时,垃圾收集器不会回收软引用的对象;只有在内存不足时,才会回收这些对象。
- 创建方式: 使用
SoftReference类来创建软引用。 - 应用场景: 适合用于实现内存敏感的缓存。
弱引用(Weak Reference)
- 概念: 弱引用程度更弱。只要 JVM 垃圾收集器扫描到它,不管当前内存空间足够与否,都会回收其引用的对象。
- 创建方式: 使用
WeakReference类来创建弱引用。 - 应用场景: 适合用于实现缓存机制,在对象不再被需要时可以自动被回收。
虚引用(Phantom Reference)
- 概念: 虚引用是所有引用类型中最弱的一个。一个拥有虚引用的对象和没有任何引用效果上是一样的,随时都可能被垃圾回收器回收。
- 创建方式: 使用
PhantomReference类来创建虚引用,并且必须和一个ReferenceQueue一起使用。 - 应用场景: 虚引用主要用于跟踪对象被垃圾回收的活动,例如资源释放前的清理工作,不同于前面的引用类型,虚引用并不决定对象的生命周期。
如何判断一个常量是废弃常量
假如在字符串常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,”abc” 就会被系统清理出常量池了。
如何判断一个类是无用的类
判定一个常量是否是“废弃常量”比较简单,而要判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类”:
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的
ClassLoader已经被回收。 - 该类对应的
java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述 3 个条件的无用类进行回收,这里说的仅仅是“可以”,而并不是和对象一样不使用了就会必然被回收。
JVM 垃圾回收算法
标记-清除算法
标记-清除(Mark-and-Sweep)算法分为“标记(Mark)”和“清除(Sweep)”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。
它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:
- 效率问题:标记和清除两个过程效率都不高。
- 空间问题:标记清除后会产生大量不连续的内存碎片。

整个标记-清除算法过程是:
- 当一个对象被创建时,给一个标记位,假设为 0 (false);
- 在标记阶段,我们将所有可达对象(或用户可以引用的对象)的标记位设置为 1 (true);
- 扫描阶段清除的就是标记位为 0 (false)的对象。
复制算法

复制算法(Copying Garbage Collection)是垃圾回收(GC)中的一种算法,它将可用内存划分为两个等大的区域:一块用于当前的内存分配(活动区域),另一块处于空闲状态(空闲区域)。工作原理如下:
- 当程序运行并分配对象时,所有的对象都先在活动区域分配。
- 当活动区域快要满时,启动垃圾回收。
- 垃圾回收过程中,首先标记活动区域中所有的活动对象(即仍然被引用的对象)。
- 接下来,复制算法将所有标记的活动对象复制到空闲区域,并同时进行内存地址的更新。
- 复制完成后,活动区域中未被复制的对象(被视为垃圾)被丢弃,活动区域成为空闲区域。
- 空闲区域和活动区域的角色互换,新的对象分配将在新的活动区域进行。
优点:
- 实现简单,回收过程中只需遍历活动对象。
- 分配内存速度快,只需移动指针即可完成分配。
- 减少内存碎片,因为活动对象在复制时会连续排列。
缺点:
- 只使用了一半的内存,因为一半内存始终处于空闲状态。
- 复制过程中需要时间暂停应用逻辑(Stop-The-World),可能会影响程序性能。
复制算法适用于对象存活率较低且可用内存较多的场景,常见于新生代的垃圾回收中。
标记-整理算法

标记-整理算法(Mark-Compact Algorithm)是垃圾回收中用于回收内存的一种方法,主要用于对象存活率较高的老年代(Old Generation)中。工作原理如下:
- 标记阶段:遍历所有可达对象,并在这些对象上做标记,表示它们是活动的,即当前还被引用的。
- 整理阶段:将所有活动对象向内存的一端移动,移动过程中清除未标记的对象,并更新引用这些对象的指针。
标记-整理算法的优点包括:
- 不会浪费内存,因为不像复制算法那样只用到一半的内存空间。
- 减少内存碎片,因为整理过程中会将对象集中排列。
缺点是:
- 整理阶段可能会导致较长的停顿时间,因为需要移动对象并更新引用。
- 实现比复制算法复杂。
标记-整理算法适用于老年代垃圾回收,因为老年代中对象的生命周期较长,存活率较高。
分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 Java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。
JDK 默认垃圾收集器(使用 java -XX:+PrintCommandLineFlags -version 命令查看):
- JDK 8:Parallel Scavenge(新生代)+ Parallel Old(老年代)
- JDK 9 ~ JDK20: G1
Serial 串行收集器
Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
新生代采用标记-复制算法,老年代采用标记-整理算法。

Serial 收集器的优点:简单而高效
Serial Old 老年代串行收集器
Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。

ParNew 并行新生代收集器
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。
新生代采用标记-复制算法,老年代采用标记-整理算法。

[!note] 垃圾回收中并发和并行的概念
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。
Parallel Scavenge 并行收集器
Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。 Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用 Parallel Scavenge 收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。
新生代采用标记-复制算法,老年代采用标记-整理算法。

Parallel Scavenge是 jdk1.8 的默认收集器
Parallel Old 并行老年代收集器
Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器和 Parallel Old 收集器。

CMS 并发标记清除收集器*
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS 收集器是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次基本上实现了让垃圾收集线程与用户线程同步工作。
算法:标记清除算法。不使用标记整理算法是为了保证清除时不影响用户线程中的工作线程,如果使用标记整理算法的话工作线程引用指向的对象地址就都变了。
回收区域:老年代
整个过程分为四个步骤:
- 初始标记: 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快;
- 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
- 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
优点:并发收集、低停顿
缺点:对 CPU 资源敏感;无法处理浮动垃圾;使用“标记-清除”算法会导致结束时有大量空间碎片产生。
从 JDK9 开始,CMS 收集器已经被弃用了。
G1 垃圾优先收集器*
G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器,以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。
G1 收集器有一下几个特点:
区域化堆内存(Region-based Heap): G1将整个堆内存划分为多个相同大小的区域(Region)。这些区域可以是 Eden、Survivor 或者 Old Generation 区域,而不是传统的连续空间。这样的设计使得 G1能更灵活地管理内存,只需进行部分区域的收集,而不是整个 Eden 区或 Old Generation 区。
并发与并行处理: G1既可以并发执行,也可以并行执行。它在运行时,一部分工作是与应用线程并发进行的,而在STW(Stop-The-World)事件中,多个GC线程可以并行工作,利用多核处理器的优势。
垃圾优先原则: G1通过分析各个区域的垃圾占比和预期收集时间,优先回收价值最大的区域(即含有最多可回收垃圾的区域)。这个特点是G1名称的由来,意味着它是一个以回收垃圾为优先目标的收集器。
可预测的停顿时间模型: G1 收集器允许设置期望的停顿时间(例如 50ms)。在执行垃圾回收时,G1 会尽量在这个时间范围内完成工作,减少应用程序的停顿时间。
G1 收集器的垃圾回收过程:
- 初始标记(Initial Marking):标记从 GC Roots 直接可达的活动对象。此阶段需要 STW 停顿。
- 并发标记(Concurrent Marking):在这个阶段,G1收集器并发地遍历堆中的对象图,标记所有可达的对象。
- 最终标记(Final Marking):处理并发阶段结束时遗留下来的少量工作。这个阶段可能需要短暂的STW停顿。
- 筛选回收(Evacuation):在这个阶段,G1 将选定的区域中的存活对象复制到其他区域,同时清理掉这些区域中的垃圾。这个阶段需要 STW 停顿,但 G1 会尽可能地限制所需时间。

G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来) 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)
ZGC
ZGC(Z Garbage Collector)是一种低延迟的垃圾回收器,是 JDK 11 引入的一项垃圾回收技术。它主要针对大内存、多核心的应用场景,旨在减少垃圾回收带来的停顿时间。
ZGC 是 JDK15 之后的默认垃圾回收器。
[!note] 主要特点
ZGC 的主要特点包括:
- 低停顿时间:ZGC 设计的目标之一是尽量减少垃圾回收带来的停顿时间。它采用了并发的垃圾回收方式,尽可能地与应用程序并发执行,以减少停顿的时间和影响。在目标范围内,单次垃圾回收的停顿时间通常不超过 10 毫秒。
- 大内存支持:ZGC 被设计为支持非常大的堆内存。它能有效地处理数十到数百 GB 大小的堆,并且具有可控的、较为恒定的停顿时间。
- 并发回收:ZGC 采用了全程并发的垃圾回收方式。它会与应用程序同时运行,通过在并发阶段遍历并标记存活对象,并在并发预备阶段处理待回收的对象。这样可以将停顿时间分散到整个应用程序运行过程中,减少对应用程序的影响。
- 空间压缩:ZGC 会对堆进行动态的空间压缩,以避免堆内存碎片化的问题。这有助于提高堆的使用效率,并减少内存的浪费。
[!note] 核心技术
ZGC 有以下几项核心技术来达成毫秒级停顿和大内存支持的目标:
- 并发标记:ZGC 采用增量式并发标记算法来实现并发垃圾回收。它不会在标记阶段产生长时间停顿,可以与用户线程并发运行。
- 粉碎压缩:ZGC 采用粉碎压缩算法来避免产生内存碎片。它会将内存按照特定大小(例如 2MB)分为多个区域,然后将存活对象连续放置在相邻区域,释放掉边界外的内存空间。这可以最大限度减少内存碎片。
- 直接内存映射:ZGC 会直接映射内存空间,而不需要进行内存分配。这可以避免统计堆内存碎片情况所带来的性能消耗。
- 微任务:ZGC 采用了微任务(Microtasks)机制来增量完成垃圾回收工作,从而不会产生长时间停顿。它会将总工作分割为多个微任务,这些微任务会在安全点(Safepoint)之间执行。
- 可扩展的堆内存:ZGC 不需要指定最小堆(Xmn)和最大堆(Xmx)大小,它可以跟踪堆内存变化并根据需要动态调整堆空间大小。这使得 ZGC 可以支持将近 4TB 的堆内存。
- 可插拔组件:ZGC 是一个独立的 GC 组件,它不依赖于 Gradle 等构建工具,可以与不同的工具或框架一起使用,这增强了其可移植性。
这些核心技术的运用使得 ZGC 可以实现毫秒级别的 GC 停顿,并支持将近 4TB 的大内存,适用于对低延迟和大内存有要求的应用场景。

