大家好,这里是架构资源栈!点击上方关注,添加“星标”,一起学习大厂前沿架构!
在 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 方法)中的引用
- 类的静态属性引用
- 常量池引用
- 活跃线程的引用
💾 方法区的回收机制
方法区主要用于存放类信息、常量、静态变量等元数据。因为内容稳定,回收频率远低于堆内对象。
主要清理目标:
- 废弃常量
- 无用类的卸载
类的卸载需要满足三个条件:
- 该类所有实例都已被回收。
- 加载该类的
ClassLoader
被回收。 - 该类对应的
Class
对象没有任何引用。
在动态代理和反射频繁使用的场景下,类卸载尤为关键。
🧹 对象“复活”?finalize() 机制揭秘
Java 提供了 finalize()
方法,类似 C++ 析构函数(但不推荐使用!)。
回收流程:
- GC 扫描发现对象不可达,第一次标记。
- 判断是否实现了
finalize()
方法,如果有,放入 F-Queue。 - 单独的线程异步执行
finalize()
,此时对象有机会“自救”,如重新与其他对象建立引用。 - 第二次 GC 扫描,如果仍不可达,则真正回收。
⚠️ finalize 的问题:
- 不确定是否会被调用
- 性能开销大
- 无法保证调用顺序
👉 最好用 try-finally
或 AutoCloseable
替代!
🧵 四种引用类型:谁能活得久?
Java 在 JDK 1.2 之后扩展了引用的概念,按照“生存能力”强弱分为:
引用类型 | 被回收时机 | 适用场景 | 存活时间 |
---|---|---|---|
强引用 | 永不回收 | 一般对象引用 | JVM 停止时终止 |
软引用 | 内存不足时回收 | 内存敏感缓存 | 内存不足时终止 |
弱引用 | GC 时立即回收 | 临时缓存、Map | GC 后即终止 |
虚引用 | 不可通过引用访问对象 | 监控、哨兵机制 | 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 发布!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。