大家好呀!我是你们的老朋友Java技术博主👋 今天咱们来聊聊Java虚拟机(JVM)中一个超级重要的话题——垃圾回收机制(Garbage Collection)和GC Root可达性分析!这可是Java程序员必须掌握的核心知识点哦!😎
🌟 前言:为什么需要垃圾回收?
想象一下你家的房间 🏠,如果从来不打扫卫生,垃圾越堆越多,最后会怎么样?没错,房间会变得又脏又乱,甚至没法住人了!😱
Java程序运行时也是这样,会不断创建对象(就像产生垃圾),如果不及时清理,内存就会被占满,程序就会"卡死"!💀
所以Java设计了"自动垃圾回收"机制,就像请了个智能保洁阿姨 🤖,会自动帮我们打扫内存,清理不再使用的对象。这就是GC(Garbage Collection)的由来!
📚 一、垃圾回收基本概念
1.1 什么是垃圾?
在Java中,"垃圾"就是指那些已经不再被使用的对象。比如:
public class Test {
public static void main(String[] args) {
String s1 = new String("hello"); // 创建对象1
s1 = new String("world"); // 对象1变成垃圾了!
}
}
上面的代码中,第一个"hello"字符串对象后来没人引用了,就变成了垃圾🗑️。
1.2 为什么要回收垃圾?
因为内存是有限的!如果不回收垃圾:
- 内存会被慢慢耗尽 💧
- 程序运行会越来越慢 🐢
- 最终导致OutOfMemoryError崩溃 💥
1.3 垃圾回收是谁来做的?
JVM中有专门的垃圾收集器(Garbage Collector) 来做这件事,不同的JDK版本有不同的实现,比如:
- Serial GC 🚂
- Parallel GC ✈️
- CMS GC �
- G1 GC 🚀
- ZGC 🛸
(具体收集器我们后面再细讲)
🔍 二、如何判断对象是垃圾?
这是垃圾回收的核心问题!JVM主要使用可达性分析算法来判断对象是否存活。
2.1 引用计数法(简单但有问题)
先来看一个简单的思路:给每个对象加个计数器,记录有多少引用指向它。
Object a = new Object(); // 对象A的引用计数=1
Object b = a; // 对象A的引用计数=2
a = null; // 对象A的引用计数=1
b = null; // 对象A的引用计数=0 → 可以回收
看起来不错?但是有个致命问题——循环引用!
class Node {
Node next;
}
Node a = new Node(); // 对象A计数=1
Node b = new Node(); // 对象B计数=1
a.next = b; // 对象B计数=2
b.next = a; // 对象A计数=2
a = null; // 对象A计数=1
b = null; // 对象B计数=1
// 但实际上A和B已经无法被访问了,却因为计数不为0无法回收!😱
所以Java没有用这种方法!
2.2 可达性分析算法(Java实际使用的)
这个算法就像在对象间玩"谁是我的朋友"的游戏 🎮:
- 首先定义一些GC Roots(相当于"顶级大佬")
- 从这些GC Roots出发,看看能"抱大腿"(被引用)的对象
- 所有能通过引用链到达的对象都是存活对象
- 其他对象就是垃圾,可以回收
[外链图片转存中…(img-u7WZTOeT-1746801753658)] (想象这是一张很生动的可达性分析图)
🌳 三、GC Roots有哪些?
GC Roots就像是对象世界的"顶级大佬",主要有以下几类:
3.1 虚拟机栈中的局部变量
public void method() {
Object obj = new Object(); // obj就是GC Root
// ...
} // 方法结束后obj不再为GC Root
3.2 方法区中的静态变量
class Test {
static Object staticObj = new Object(); // staticObj是GC Root
}
3.3 方法区中常量引用的对象
class Test {
static final String CONSTANT = "constant"; // CONSTANT是GC Root
}
3.4 本地方法栈中JNI引用的对象
public native void nativeMethod(Object obj); // native方法引用的对象
3.5 同步锁持有的对象
synchronized(lockObject) { // lockObject是GC Root
// ...
}
3.6 虚拟机内部引用
比如基本类型对应的Class对象、常驻的异常对象等。
🧐 四、可达性分析详细过程
让我们用一个超详细的例子来看看可达性分析是怎么工作的!
class Person {
String name;
Person friend;
Person(String name) {
this.name = name;
}
}
public class GCDemo {
static Person p1 = new Person("张三"); // GC Root
public static void main(String[] args) {
Person p2 = new Person("李四"); // GC Root (栈局部变量)
Person p3 = new Person("王五"); // GC Root (栈局部变量)
p1.friend = p2;
p2.friend = p3;
p3.friend = new Person("赵六");
p2 = null; // 断开李四的引用
}
}
现在内存中的对象关系是这样的:
GC Roots:
- 静态变量 p1 → 张三
- 局部变量 p3 → 王五
对象引用链:
张三 → 李四 → 王五 → 赵六
王五 (直接由p3引用)
可达性分析步骤:
从GC Roots出发:
- p1(张三)可达
- p3(王五)可达
扫描张三的引用:
- 张三 → 李四 (可达)
- 李四 → 王五 (但王五已经被标记)
- 王五 → 赵六 (可达)
最终存活对象:
- 张三、李四、王五、赵六
其他对象:无(这个例子中所有对象都可达)
如果把代码改成:
p3 = null; // 断开王五的引用
那么对象链就变成:
张三 → 李四 → 王五 → 赵六
但没有任何GC Roots能到达王五和赵六了,所以:
- 存活对象:张三、李四
- 可回收对象:王五、赵六
⏳ 五、对象的生死历程
一个对象在垃圾回收中的"一生"是这样的:
- 可达的:从GC Roots能到达
- 可复活:重写了finalize()方法且未被调用过
- 不可达的:从GC Roots无法到达
- 真正死亡:finalize()后仍然不可达
特别要注意finalize()
方法:
protected void finalize() throws Throwable {
// 对象被回收前的最后挣扎
// 可以在这里让对象重新被引用"复活"
}
但是!⚠️ 强烈建议不要依赖finalize()!因为:
- 调用时机不确定
- 性能差
- 可能根本不被调用
🧹 六、垃圾回收算法
知道了哪些是垃圾后,JVM会用以下几种方式清理:
6.1 标记-清除 (Mark-Sweep)
- 标记所有存活对象
- 清除所有未标记对象
✅ 优点:简单直接
❌ 缺点:产生内存碎片
6.2 标记-整理 (Mark-Compact)
- 标记存活对象
- 移动存活对象到内存一端
- 清理边界外内存
[外链图片转存中…(img-hQAmW4wT-1746801753659)]
✅ 优点:没有碎片
❌ 缺点:移动对象成本高
6.3 复制算法 (Copying)
把内存分成两块,只用其中一块:
- 将存活对象复制到另一块
- 清空当前块
✅ 优点:没有碎片、简单高效
❌ 缺点:浪费一半内存
6.4 分代收集 (Generational)
这是现代JVM最常用的方法!根据对象存活时间把堆分成:
新生代 (Young Generation) - 新创建的对象
- 使用复制算法
- 分为Eden、Survivor0、Survivor1区
老年代 (Tenured Generation) - 长期存活的对象
- 使用标记-清除或标记-整理
永久代/元空间 (PermGen/Metaspace) - 类信息等
- JDK8后用元空间替代永久代
对象晋升过程:
新生对象 → Eden区 → 第一次GC后 → Survivor区 → 多次GC后 → 老年代
🚗 七、常见垃圾收集器
JVM提供了多种垃圾收集器,就像不同的清洁车:
7.1 Serial收集器 🚜
- 单线程工作
- 适合客户端小应用
- 会"Stop The World"(暂停所有应用线程)
7.2 Parallel收集器 ✈️
- Serial的多线程版本
- JDK8默认收集器
- 注重吞吐量
7.3 CMS收集器 🚒
- 并发标记清除
- 减少停顿时间
- JDK9开始被废弃
7.4 G1收集器 🚀
- JDK9后默认收集器
- 把堆分成多个Region
- 可预测停顿时间
7.5 ZGC收集器 🛸
- JDK11引入
- 超低延迟(<10ms)
- 处理超大堆
🛠️ 八、GC调优基础
虽然GC是自动的,但我们也可以适当调优:
8.1 常用JVM参数
-Xms
/-Xmx
:初始/最大堆大小-Xmn
:新生代大小-XX:+UseG1GC
:使用G1收集器-XX:MaxGCPauseMillis=200
:目标最大GC停顿
8.2 调优原则
- 优先让JVM自动调整
- 先满足性能要求,再考虑减少内存
- 监控GC日志是关键!
8.3 查看GC日志
添加JVM参数:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
💡 九、内存泄漏排查技巧
即使有GC,也可能发生内存泄漏!常见原因:
- 静态集合持有对象
- 未关闭的资源(连接、流等)
- 监听器未注销
- 不合理的缓存
排查工具:
jmap
:生成堆转储jvisualvm
:可视化分析Eclipse MAT
:强大的内存分析工具
📝 十、总结
- 垃圾回收是JVM自动内存管理机制 🧹
- 可达性分析通过GC Roots判断对象存活 🌳
- GC Roots包括:栈局部变量、静态变量等 🔍
- 主要回收算法:标记-清除、复制、分代 ♻️
- 不同收集器适用于不同场景 🚀
- 适当调优可以提升性能 ⚡
记住,理解GC原理对于写出高性能Java程序非常重要!