《JVM如何判断一个对象可以被回收?图文详解GC Root算法》

发布于:2025-05-19 ⋅ 阅读:(17) ⋅ 点赞:(0)

大家好呀!我是你们的老朋友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实际使用的)

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

  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 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引用)

可达性分析步骤:

  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程序非常重要!

推荐阅读文章


网站公告

今日签到

点亮在社区的每一天
去签到