大家好,这里是架构资源栈!点击上方关注,添加“星标”,一起学习大厂前沿架构!

在 Java 的世界里,垃圾回收(Garbage Collection, GC) 是开发者绕不开的话题。它默默守护着程序的内存安全,但稍有疏忽就可能引发内存泄漏、卡顿甚至系统崩溃。

本篇文章从对象的“生死判定”垃圾回收算法的演进,再到Stop-The-World 与 SafePoint 的原理,一文带你彻底搞懂 Java 的 GC 机制!


💡 哪些区域需要 GC?

垃圾收集主要针对 方法区

而这三大线程私有区域:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

由于随线程的生命周期自动释放,不需要 GC 介入。


🔍 如何判断对象是否可以被回收?

Java 中,几乎所有的对象实例都在堆中分配。GC 要做的第一件事就是判断哪些对象“已经死了”。

🧮 引用计数算法(Reference Counting)

思路很简单:每个对象维护一个引用计数器,引用加 1,释放减 1,计数为 0 时认为对象“无主”。

但问题来了:

public class Test {
    public Object instance = null;

    public static void main(String[] args) {
        Test a = new Test();
        Test b = new Test();
        a.instance = b;
        b.instance = a;
        a = null;
        b = null;
        doSomething();
    }
}

两个对象互相引用,即使外部都已断开,它们的引用计数永远不为 0,造成内存泄漏

👉 因此 Java 并不采用引用计数算法


🔗 可达性分析算法(Reachability Analysis) ✅

这是 Java 真正使用的算法。

从一组称为 GC Roots 的对象出发,沿着对象引用链遍历,能到达的对象就是存活的,不能到达的就是“垃圾”

典型 GC Roots 包括:

  • 虚拟机栈中引用的对象
  • JNI(Native 方法)中的引用
  • 类的静态属性引用
  • 常量池引用
  • 活跃线程的引用

💾 方法区的回收机制

方法区主要用于存放类信息、常量、静态变量等元数据。因为内容稳定,回收频率远低于堆内对象。

主要清理目标:

  • 废弃常量
  • 无用类的卸载

类的卸载需要满足三个条件:

  1. 该类所有实例都已被回收。
  2. 加载该类的 ClassLoader 被回收。
  3. 该类对应的 Class 对象没有任何引用。
在动态代理和反射频繁使用的场景下,类卸载尤为关键。

🧹 对象“复活”?finalize() 机制揭秘

Java 提供了 finalize() 方法,类似 C++ 析构函数(但不推荐使用!)。

回收流程:

  1. GC 扫描发现对象不可达,第一次标记
  2. 判断是否实现了 finalize() 方法,如果有,放入 F-Queue。
  3. 单独的线程异步执行 finalize(),此时对象有机会“自救”,如重新与其他对象建立引用。
  4. 第二次 GC 扫描,如果仍不可达,则真正回收。

⚠️ finalize 的问题:

  • 不确定是否会被调用
  • 性能开销大
  • 无法保证调用顺序

👉 最好用 try-finallyAutoCloseable 替代!


🧵 四种引用类型:谁能活得久?

Java 在 JDK 1.2 之后扩展了引用的概念,按照“生存能力”强弱分为:

引用类型被回收时机适用场景存活时间
强引用永不回收一般对象引用JVM 停止时终止
软引用内存不足时回收内存敏感缓存内存不足时终止
弱引用GC 时立即回收临时缓存、MapGC 后即终止
虚引用不可通过引用访问对象监控、哨兵机制GC 后即终止

代码示例:

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<>(obj); // 软引用
WeakReference<Object> wf = new WeakReference<>(obj); // 弱引用
PhantomReference<Object> pf = new PhantomReference<>(obj, new ReferenceQueue<>()); // 虚引用

♻️ 常见的 GC 算法

1️⃣ 标记-清除(Mark-Sweep)

两阶段:

  • 标记:找出所有存活对象
  • 清除:回收未被标记的对象

缺点:

  • 易产生内存碎片
  • 效率不高

2️⃣ 标记-整理(Mark-Compact)

将存活对象移动到一端,清理边界外内存,避免内存碎片

适合老年代,缺点是移动对象开销大。


3️⃣ 复制算法(Copying)

将内存分为两块,每次只用一半。

GC 时把存活对象复制到另一块,然后清理原区域。

新生代经典应用:Eden + 两个 Survivor(8:1:1)

  • 每次 GC 使用 Eden + 一个 Survivor
  • 存活对象复制到另一个 Survivor
  • Survivor 不够用就进入老年代(晋升)

4️⃣ 分代收集(Generational GC)

不同“年龄”的对象采用不同算法:

区域特点回收算法
新生代对象存活率低复制算法
老年代对象生命周期长标记-清除 / 标记-整理

🛑 Stop-The-World 与 SafePoint 深度揭秘

Stop-The-World(STW)

进行 GC 时,必须暂停所有 Java 应用线程,这就是 STW。

原因:

  • GC Roots 遍历必须保证对象引用状态不变
  • 否则无法确保 GC 的准确性

无论使用哪种 GC 收集器(包括 G1、ZGC),STW 都无法完全避免,只能尽量缩短时间。

👉 不推荐调用 System.gc(),它会强制触发 GC 导致 STW


SafePoint:程序可被 GC 暂停的点

程序不是任何时刻都能被 GC 中断的,只有在特定代码位置才行,这些点叫 SafePoint

常见 SafePoint:

  • 方法调用
  • 循环跳转
  • 异常跳转

设计 SafePoint 的策略:

  • 太少 → STW 等待太久
  • 太多 → 影响程序执行性能

🚀 减少 GC 停顿的两种策略

✅ 增量收集(Incremental GC)

将 GC 划分为多个小阶段,穿插在应用线程之间,减少每次 STW 时间

缺点:线程切换 & 上下文切换频繁,吞吐量下降


✅ 分区算法(Region-based)

将堆划分成多个小分区,每次只回收其中一部分,控制 GC 停顿时间。

注意:

  • 分区算法 ≠ 分代收集
  • 分代是按对象年龄分类,分区是按物理内存块划分

✅ 总结

本篇内容干货满满,总结如下:

  • 对象是否能被回收,用 可达性分析算法判断;
  • Java 引用分为:强、软、弱、虚;
  • 常见 GC 算法:标记-清除、标记-整理、复制、分代;
  • GC 时会触发 Stop-The-World,只在 SafePoint 执行;
  • 优化 GC 停顿:增量收集 + 分区算法

🎯 如果你正在:

  • 排查内存泄漏
  • 优化 GC 暂停时间
  • 面试 JVM 原理相关问题

这篇文章都是你值得收藏 & 转发的技术宝典!


本文由博客一文多发平台 OpenWrite 发布!

吾日三省吾码
31 声望4 粉丝