贝斯特365-365提前结束投注-365bet中国客服电话

Java 垃圾回收 (GC) 全面解析!

Java 垃圾回收 (GC) 全面解析!

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

在 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 sf = new SoftReference<>(obj); // 软引用

WeakReference wf = new WeakReference<>(obj); // 弱引用

PhantomReference 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 原理相关问题

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

相关推荐