在 JVM 中,程序计数器、虚拟机栈和本地方法栈这三个区域的生命周期与线程紧密绑定,其内存分配和释放的时机是明确的,因此不需要垃圾回收的介入。真正需要动态管理的是 Java 堆和方法区(元空间),它们的内存使用在程序运行期间不断变化,这也是垃圾回收器的主要工作区域。
对象引用
为了更好地管理对象的生命周期和辅助垃圾回收,Java 提供了四种不同强度的引用类型:
- 强引用:最常见的引用形式,例如
Object obj = new Object();。只要强引用关系存在,垃圾收集器就永远不会回收被引用的对象,即使在内存不足时,JVM 宁愿抛出 OutOfMemoryError 异常也不会去回收它们。
- 软引用:通过
SoftReference 类实现。被软引用关联的对象,在内存充足时不会被回收;只有在系统即将发生内存溢出异常之前,JVM 才会尝试回收这些对象。它常被用来实现内存敏感的缓存。
- 弱引用:通过
WeakReference 类实现。无论当前内存是否充足,只要发生垃圾回收,弱引用所指向的对象都会被回收。它适用于构建那种不阻止其键被回收的映射表,例如 WeakHashMap。
- 虚引用:通过
PhantomReference 类实现,并且必须与引用队列 ReferenceQueue 联合使用。虚引用完全无法通过它获取到对象的实例,其唯一目的是在对象被回收时收到一个系统通知,常用于跟踪对象的垃圾回收过程或执行一些资源清理工作。
| 引用类型 |
被垃圾回收时机 |
用途 |
生存时间 |
| 强引用 |
从来不会 |
对象的一般状态 |
JVM 停止运行时终止 |
| 软引用 |
当内存不足时 |
对象缓存 |
内存不足时终止 |
| 弱引用 |
正常垃圾回收时 |
对象缓存 |
垃圾回收后终止 |
| 虚引用 |
正常垃圾回收时 |
跟踪对象的垃圾回收 |
垃圾回收后终止 |
垃圾对象判定方法
引用计数法
引用计数法为每个对象维护一个引用计数器。每当有一个地方引用它时,计数器加一;当引用失效时,计数器减一。任何时刻计数器为零的对象就是不可能再被使用的,即“垃圾”。
这种方法虽然简单高效,但存在一个致命的缺陷:它无法解决对象之间相互循环引用的问题。例如,对象 A 引用 B,对象 B 也引用 A,除此之外它们再无任何外部引用。此时它们的引用计数都不为零,但实际上这两个对象已经无法被程序访问。因此,主流的 JVM(如 HotSpot)都没有选用引用计数法来管理 内存管理。
根搜索算法
根搜索算法,也称为可达性分析算法,是当前主流的对象存活判定方法。它的基本思路是通过一系列称为 “GC Roots” 的根对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为“引用链”。如果一个对象到 GC Roots 没有任何引用链相连,则证明此对象是不可用的,可以被判定为垃圾。
哪些对象可以作为 GC Roots 呢?通常包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 本地方法栈中 JNI(即 Native 方法)引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象,比如字符串常量池里的引用。
- 被同步锁
synchronized 持有的对象。
- JVM 内部的引用,如基本数据类型对应的 Class 对象、一些常驻的异常对象等。
实际上,即使在可达性分析算法中不可达的对象,也并非“非死不可”。它们会经历两次标记过程:第一次标记后,如果对象覆盖了 finalize() 方法且未被虚拟机调用过,它会被放入一个名为 F-Queue 的队列中。稍后由一个低优先级的 Finalizer 线程去执行队列中对象的 finalize() 方法。如果对象在 finalize() 中成功重新与引用链上的任何一个对象建立关联,那它将在第二次标记时被移出“即将回收”的集合,否则就会被真正回收。
垃圾回收算法
标记-清除算法
标记-清除算法 是最基础和直接的垃圾收集算法,它分为两个阶段:
- 标记阶段:从 GC Roots 出发,遍历所有可达对象,并对这些存活对象进行标记。
- 清除阶段:遍历整个堆内存,将未被标记的对象(即不可达对象)统一回收,释放其占用的内存空间。

优点:
- 实现简单:逻辑清晰,易于理解和实现。
- 适用于老年代:在对象存活率较高的区域(如老年代),无需频繁移动对象。
缺点:
- 产生内存碎片:清除后会产生大量不连续的内存碎片,可能导致后续无法找到足够大的连续空间来分配大对象,从而提前触发另一次垃圾收集。
- 效率不稳定:标记和清除两个过程的效率都随对象数量的增加而下降,当堆中对象非常多时,性能开销较大。
JVM 应用:尽管存在碎片问题,标记-清除算法仍是许多现代垃圾收集器的基础。例如,CMS 收集器的核心就采用了(并发)标记-清除算法。
复制算法
复制算法 的出现是为了解决标记-清除算法导致的碎片化问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当正在使用的这块内存用完了,就将还存活着的对象复制到另一块空闲的内存上,然后把已使用过的内存空间一次清理掉。

优点:
- 无内存碎片:每次回收后,存活对象被紧凑地排列在内存一端,分配新对象时只需要移动堆顶指针,非常高效。
- 实现简单,运行高效:只需要遍历一次并复制存活对象,回收过程很快。
- 适合对象生命周期短的场景:尤其适用于新生代,因为绝大部分对象都是“朝生夕死”,每次回收只需要复制少量的存活对象。
缺点:
- 内存利用率低:在任何时候,总有一半的内存是闲置的,空间代价高昂。
- 不适合存活率高的区域:如果区域中大多数对象都是存活的,那么复制的开销会非常大。
JVM 应用:复制算法被广泛应用于 JVM 的新生代垃圾回收。为了优化空间利用率,HotSpot 虚拟机将新生代划分为一个较大的 Eden 区和两个较小的 Survivor 区(From 和 To),通常比例为 8:1:1。每次 Minor GC 时,将 Eden 和 From Survivor 中存活的对象复制到 To Survivor 区。
标记-整理算法
标记-整理算法 是针对老年代对象存活率高的特点,在标记-清除算法基础上进行的改进。它也分为三个阶段:
- 标记阶段:与标记-清除相同,从 GC Roots 出发,遍历并标记所有存活对象。
- 整理阶段:将所有存活的对象向内存空间的一端进行移动,使它们紧凑地排列在一起。
- 清除阶段:直接清理掉边界以外的所有内存。

优点:
- 无内存碎片:存活对象被整理到连续的内存区域,解决了碎片化问题。
- 适合存活率高的区域:在老年代中,对象大多长期存活,移动存活对象的成本相对于复制整个区域要低得多。
缺点:
- 整理开销大:移动存活对象并更新所有指向这些对象的引用,是一项需要暂停用户线程(Stop-The-World)的操作,且对象越多,暂停时间可能越长。
- 实现复杂度高:相比前两种算法,其逻辑相对复杂。
JVM 应用:标记-整理算法是老年代回收器的常用选择,如 Serial Old 和 Parallel Old 收集器都采用了此算法。
分代回收机制
现代商用 JVM 的垃圾收集器大多采用了 分代回收机制。这个设计基于两个经验性的“分代假说”:
- 弱分代假说:绝大多数对象都是朝生夕死的,生命周期极短。
- 强分代假说:熬过越多次垃圾收集过程的对象,就越难以消亡。
基于这两个假说,JVM 将堆内存划分为新生代和老年代,并对不同“代”采用不同的垃圾收集策略。

- 新生代:存放新创建的对象。区域进一步细分为一个 Eden 区和两个 Survivor 区(From/To)。新对象优先在 Eden 区分配。当 Eden 区满时,会触发一次 Minor GC(或 Young GC),采用复制算法将 Eden 和 From Survivor 中存活的对象复制到 To Survivor 区,并清空 Eden 和 From Survivor。每次在 Survivor 区中熬过一次 Minor GC,对象的年龄就增加一岁。当年龄达到阈值(默认15)或 Survivor 区空间不足时,对象会晋升到老年代。此外,过大的对象(可通过
-XX:PretenureSizeThreshold 参数设置)可能直接进入老年代。
- 老年代:存放长期存活的对象。当老年代空间不足时(可能由对象晋升、大对象直接进入等触发),会触发 Major GC(或 Full GC),通常采用标记-清除或标记-整理算法。Major GC 的速度一般比 Minor GC 慢10倍以上。
- 方法区/元空间:用于存储已被虚拟机加载的类信息、常量、静态变量等。在 JDK 7 及之前通过“永久代”实现,JDK 8 开始被“元空间”取代。此区域的垃圾收集主要回收废弃的常量和不再使用的类型,发生在 Full GC 时,采用标记-清除算法,触发频率很低。
- Full GC:指对整个堆(新生代 + 老年代)以及方法区进行的全面垃圾收集。通常由老年代满、方法区满、调用
System.gc() 等原因触发。Full GC 会暂停所有应用线程(Stop-The-World),停顿时间很长,应尽量避免。
分代回收是现代 JVM 大多数垃圾收集器(如 Serial, Parallel, CMS)的基础架构。注意,G1 收集器在逻辑上仍然分代,但在物理内存上是不连续的分区;而 ZGC 和 Shenandoah 则更进一步弱化了分代的概念。
JVM 常用垃圾收集器
下面是 HotSpot 虚拟机(JDK 8 ~ 17 常见版本)中几种主流的垃圾收集器:
| 垃圾回收器 |
适用代 |
并行 / 并发 |
是否 Stop-The-World |
算法基础 |
优点 |
缺点 |
适用场景 |
| Serial |
新生代 |
单线程 |
是(全程) |
复制算法 |
简单高效,内存开销小 |
单线程,暂停时间长 |
客户端模式、小型应用 |
| Serial Old |
老年代 |
单线程 |
是(全程) |
标记-整理 |
与 Serial 配合使用 |
单线程,性能差 |
Client 模式默认;CMS 失败后备 |
| ParNew |
新生代 |
多线程并行 |
是(仅 STW 阶段) |
复制算法 |
多线程加速 Minor GC |
仅能与 CMS 配合 |
需低延迟 + CMS 老年代 |
| Parallel Scavenge |
新生代 |
多线程并行 |
是 |
复制算法 |
高吞吐量,自动调优 |
不关注停顿时间 |
后台计算、批处理任务 |
| Parallel Old |
老年代 |
多线程并行 |
是 |
标记-整理 |
高吞吐,与 Parallel Scavenge 搭配 |
停顿时间较长 |
吞吐优先型应用 |
| CMS (Concurrent Mark Sweep) |
老年代 |
并发(部分阶段) |
初始标记 & 重新标记阶段 STW |
标记-清除 |
低停顿,适合响应敏感应用 |
CPU 敏感、产生碎片、JDK 14+ 已移除 |
Web 服务、低延迟要求 |
| G1 (Garbage-First) |
整堆(逻辑分代) |
并发 + 并行 |
有(但可预测) |
分区 + 复制 + 标记-整理 |
可预测停顿时间、无碎片、大堆支持 |
配置复杂,小堆性能不如 CMS/Parallel |
JDK 9+ 默认,大堆(>4GB)、低延迟 |
| ZGC |
整堆 |
几乎全并发 |
极短(<1ms) |
着色指针 + 读屏障 |
超低延迟(<10ms),支持 TB 级堆 |
JDK 15+ 才生产就绪,需较新硬件 |
超大堆、极致低延迟场景 |
说明:
- 并行:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
- 并发:指垃圾收集线程与用户线程同时(或交替)执行,目的是缩短用户线程的停顿时间。
JDK 默认回收器演进:
- JDK 8:Parallel Scavenge (新生代) + Parallel Old (老年代)
- JDK 9 ~ 13:G1
- JDK 14+:G1 (ZGC 作为实验性功能或可选)
选择建议:
- 追求吞吐量:Parallel Scavenge + Parallel Old 组合。
- 追求低延迟:对于通用场景,G1 是很好的选择;对于超大堆和极致低延迟场景,可以考虑 ZGC。
- 资源受限或嵌入式环境:Serial + Serial Old 组合。
理解这些基础概念和收集器特点,是进行 JVM 性能调优的第一步。如果你想深入探讨某个特定收集器的工作原理或调优案例,欢迎在 云栈社区 与大家交流。