当前位置: 首页 > news >正文

《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实际使用的)

这个算法就像在对象间玩"谁是我的朋友"的游戏 🎮:

  1. 首先定义一些GC Roots(相当于"顶级大佬")
  2. 从这些GC Roots出发,看看能"抱大腿"(被引用)的对象
  3. 所有能通过引用链到达的对象都是存活对象
  4. 其他对象就是垃圾,可以回收

[外链图片转存中…(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引用)

可达性分析步骤:

  1. 从GC Roots出发:

    • p1(张三)可达
    • p3(王五)可达
  2. 扫描张三的引用:

    • 张三 → 李四 (可达)
    • 李四 → 王五 (但王五已经被标记)
    • 王五 → 赵六 (可达)
  3. 最终存活对象:

    • 张三、李四、王五、赵六
  4. 其他对象:无(这个例子中所有对象都可达)

如果把代码改成:

p3 = null;  // 断开王五的引用

那么对象链就变成:

张三 → 李四 → 王五 → 赵六

但没有任何GC Roots能到达王五和赵六了,所以:

  • 存活对象:张三、李四
  • 可回收对象:王五、赵六

⏳ 五、对象的生死历程

一个对象在垃圾回收中的"一生"是这样的:

  1. 可达的:从GC Roots能到达
  2. 可复活:重写了finalize()方法且未被调用过
  3. 不可达的:从GC Roots无法到达
  4. 真正死亡:finalize()后仍然不可达

特别要注意finalize()方法:

protected void finalize() throws Throwable {// 对象被回收前的最后挣扎// 可以在这里让对象重新被引用"复活"
}

但是!⚠️ 强烈建议不要依赖finalize()!因为:

  • 调用时机不确定
  • 性能差
  • 可能根本不被调用

🧹 六、垃圾回收算法

知道了哪些是垃圾后,JVM会用以下几种方式清理:

6.1 标记-清除 (Mark-Sweep)

  1. 标记所有存活对象
  2. 清除所有未标记对象

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

✅ 优点:简单直接
❌ 缺点:产生内存碎片

6.2 标记-整理 (Mark-Compact)

  1. 标记存活对象
  2. 移动存活对象到内存一端
  3. 清理边界外内存

[外链图片转存中…(img-hQAmW4wT-1746801753659)]

✅ 优点:没有碎片
❌ 缺点:移动对象成本高

6.3 复制算法 (Copying)

把内存分成两块,只用其中一块:

  1. 将存活对象复制到另一块
  2. 清空当前块

✅ 优点:没有碎片、简单高效
❌ 缺点:浪费一半内存

6.4 分代收集 (Generational)

这是现代JVM最常用的方法!根据对象存活时间把堆分成:

  1. 新生代 (Young Generation) - 新创建的对象

    • 使用复制算法
    • 分为Eden、Survivor0、Survivor1区
  2. 老年代 (Tenured Generation) - 长期存活的对象

    • 使用标记-清除或标记-整理
  3. 永久代/元空间 (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 调优原则

  1. 优先让JVM自动调整
  2. 先满足性能要求,再考虑减少内存
  3. 监控GC日志是关键!

8.3 查看GC日志

添加JVM参数:

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log

💡 九、内存泄漏排查技巧

即使有GC,也可能发生内存泄漏!常见原因:

  1. 静态集合持有对象
  2. 未关闭的资源(连接、流等)
  3. 监听器未注销
  4. 不合理的缓存

排查工具:

  • jmap:生成堆转储
  • jvisualvm:可视化分析
  • Eclipse MAT:强大的内存分析工具

📝 十、总结

  1. 垃圾回收是JVM自动内存管理机制 🧹
  2. 可达性分析通过GC Roots判断对象存活 🌳
  3. GC Roots包括:栈局部变量、静态变量等 🔍
  4. 主要回收算法:标记-清除、复制、分代 ♻️
  5. 不同收集器适用于不同场景 🚀
  6. 适当调优可以提升性能 ⚡

记住,理解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 的“暗坑”与解决方案(二)

相关文章:

  • 深度解析:Redis 性能优化全方位指南
  • Python操作PDF书签详解 - 添加、修改、提取和删除
  • AI量化交易是什么?它是如何重塑金融世界的?
  • 如何评估开源商城小程序源码的基础防护能力?
  • 蓝桥杯2300 质数拆分
  • 四:操作系统cpu调度之调度算法
  • JVM类加载机制
  • Java设计模式之桥接模式:从入门到精通
  • 1-3V升3.2V升压驱动WT7013
  • 利用ffmpeg截图和生成gif
  • esp32课设记录(四)摩斯密码的实现 并用mqtt上传
  • fnOS手机APP+NAS架构:破解跨地域数据实时访问的内网穿透难题
  • 5月19日笔记
  • lammps后处理:堆垛层错和孪晶的数量统计
  • 03 接口自动化-精通Postman之接口鉴权,接口Mock,接口加解密以及接口签名Sign
  • 仿腾讯会议——音频服务器部分
  • 5:OpenCV—图像亮度、对比度变换
  • 问题|对只允许输入的变量是否进行了更改
  • 禁止在Windows命令行输入python后跳转Microsoft Store
  • 使用 Terraform 创建 Azure Databricks
  • 安徽凤阳县明中都鼓楼楼宇顶部瓦片部分脱落,无人员伤亡
  • 海南乐城管理局原局长贾宁已赴省政协工作,曾从河南跨省任职
  • 建筑瞭望|从黄浦江畔趸船改造看航运设施的升级与利用
  • 宫崎骏的折返点
  • 假冒政府机构账号卖假货?“假官号”为何屡禁不绝?媒体调查
  • 从《缶翁的世界》开始,看吴昌硕等湖州籍书画家对海派的影响