《JVM如何判断一个对象可以被回收?图文详解GC Root算法》
大家好呀!我是你们的老朋友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"); // 创建对象1s1 = 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计数=2a = 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 Rootpublic 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程序非常重要!
推荐阅读文章
-
由 Spring 静态注入引发的一个线上T0级别事故(真的以后得避坑)
-
如何理解 HTTP 是无状态的,以及它与 Cookie 和 Session 之间的联系
-
HTTP、HTTPS、Cookie 和 Session 之间的关系
-
什么是 Cookie?简单介绍与使用方法
-
什么是 Session?如何应用?
-
使用 Spring 框架构建 MVC 应用程序:初学者教程
-
有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误
-
如何理解应用 Java 多线程与并发编程?
-
把握Java泛型的艺术:协变、逆变与不可变性一网打尽
-
Java Spring 中常用的 @PostConstruct 注解使用总结
-
如何理解线程安全这个概念?
-
理解 Java 桥接方法
-
Spring 整合嵌入式 Tomcat 容器
-
Tomcat 如何加载 SpringMVC 组件
-
“在什么情况下类需要实现 Serializable,什么情况下又不需要(一)?”
-
“避免序列化灾难:掌握实现 Serializable 的真相!(二)”
-
如何自定义一个自己的 Spring Boot Starter 组件(从入门到实践)
-
解密 Redis:如何通过 IO 多路复用征服高并发挑战!
-
线程 vs 虚拟线程:深入理解及区别
-
深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别
-
10大程序员提升代码优雅度的必杀技,瞬间让你成为团队宠儿!
-
“打破重复代码的魔咒:使用 Function 接口在 Java 8 中实现优雅重构!”
-
Java 中消除 If-else 技巧总结
-
线程池的核心参数配置(仅供参考)
-
【人工智能】聊聊Transformer,深度学习的一股清流(13)
-
Java 枚举的几个常用技巧,你可以试着用用
-
由 Spring 静态注入引发的一个线上T0级别事故(真的以后得避坑)
-
如何理解 HTTP 是无状态的,以及它与 Cookie 和 Session 之间的联系
-
HTTP、HTTPS、Cookie 和 Session 之间的关系
-
使用 Spring 框架构建 MVC 应用程序:初学者教程
-
有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误
-
Java Spring 中常用的 @PostConstruct 注解使用总结
-
线程 vs 虚拟线程:深入理解及区别
-
深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别
-
10大程序员提升代码优雅度的必杀技,瞬间让你成为团队宠儿!
-
探索 Lombok 的 @Builder 和 @SuperBuilder:避坑指南(一)
-
为什么用了 @Builder 反而报错?深入理解 Lombok 的“暗坑”与解决方案(二)