JVM 如何判断‘对象 / 类该回收

一、核心问题:JVM 怎么判断 "对象死了"?(垃圾判定算法)

堆里的对象成千上万,GC 首先要解决 "哪些是垃圾(不能再用)" 的问题,主流有两种算法,重点是可达性分析算法(现在所有 JVM 都在用)。

1. 引用计数法:简单但有致命缺陷
核心逻辑:

给每个对象装一个 "计数器",记录当前有多少地方在引用它:

  • 有人引用它(比如 A a = new A()),计数器 + 1;
  • 引用失效(比如 a = null),计数器 - 1;
  • 计数器 = 0 → 判定为垃圾,等待回收。
优点:

实现超级简单,计算速度快(不用遍历整个引用链)。

致命缺陷:解决不了 "循环引用"

这就是它被淘汰的原因!比如你给的代码:

java 复制代码
ReferenceCountingGc objA = new ReferenceCountingGc(); // objA引用objA对象,计数器=1
ReferenceCountingGc objB = new ReferenceCountingGc(); // objB引用objB对象,计数器=1
objA.instance = objB; // objA对象引用objB对象,objB计数器+1 → 2
objB.instance = objA; // objB对象引用objA对象,objA计数器+1 → 2
objA = null; // 原objA变量的引用失效,objA对象计数器-1 → 1
objB = null; // 原objB变量的引用失效,objB对象计数器-1 → 1

此时,objAobjB对象已经没有任何 "外部引用"(程序再也用不到它们了),但因为互相引用,计数器都是 1,引用计数法会误以为它们 "还在被使用",永远不会回收 ------ 这会导致内存泄漏(没用的对象占着内存不放)。

2. 可达性分析算法:JVM 的 "标准答案"
核心逻辑:

用 "引用链追踪" 代替计数器,核心是 "从根节点出发,能找到的就是活的,找不到的就是死的":

  1. 确定一批 "GC Roots"(根对象)------ 这些对象是 "绝对不可能被回收" 的(相当于仓库的 "承重墙",不能动);
  2. 从 GC Roots 出发,像 "寻宝" 一样向下遍历所有被它们引用的对象,形成一条 "引用链";
  3. 能被遍历到的对象(有引用链连接)→ 活着;遍历不到的对象(断了引用链)→ 垃圾。
关键:哪些是 GC Roots?(必记)

你可以理解为 "JVM 认定的、不会被回收的核心对象",主要包括:

  • 线程栈的本地变量:比如方法里的局部变量(main方法里的objAobjB)、方法参数;
  • 静态变量:类级别的变量(public static User user),属于类,只要类还在,它就活着;
  • 本地方法栈的变量:调用 native 方法(比如 Java 调用 C/C++ 代码)时用到的变量;
  • 活跃线程对象:正在运行的线程本身(线程没结束,它的相关资源不能回收);
  • 方法区里的常量:比如String常量池里的字符串("abc")。
解决循环引用问题:

回到之前的循环引用案例:

  • objA = nullobjB = null后,objAobjB对象不再被任何 GC Roots 引用(它们的引用链断了);
  • 即使它们互相引用,可达性分析算法也会判定它们是 "不可达" 的 → 标记为垃圾,后续会被回收。

这就是为什么可达性分析算法能成为主流 ------ 彻底解决了循环引用的坑。

二、补充:Java 的 4 种引用类型(影响对象 "存活优先级")

可达性分析里的 "引用" 不是 "非有即无" 的,Java 把引用分成 4 种强度,强度从高到低,决定了对象被 GC 回收的 "难易程度"------ 这是对 "垃圾判定" 的补充:不同引用类型的对象,GC 对待它们的策略不一样。

引用类型 核心特点 实际用途 示例代码
强引用 最普通的引用(我们平时写的),GC 绝对不回收 绝大多数业务对象(比如UserList User user = new User();
软引用 内存足够时不回收,内存不够(要 OOM)时才回收 内存敏感的缓存(比如浏览器缓存、图片缓存) SoftReference<User> user = new SoftReference<>(new User());
弱引用 不管内存够不够,GC 一触发就回收 偶尔用的缓存(比如临时数据,丢了也没关系) WeakReference<User> user = new WeakReference<>(new User());
虚引用 最弱的引用,几乎等同于没有引用,不能通过它访问对象 唯一用途:跟踪对象被 GC 回收的时刻(比如记录日志) PhantomReference<User> user = new PhantomReference<>(new User(), 引用队列);
通俗解释:
  • 强引用:"刚需房"------ 只要有人住(有引用),绝对不拆;
  • 软引用:"廉租房"------ 内存够就住,内存紧张时就被清退;
  • 弱引用:"日租房"------ 当天(GC 触发)就清退,不管有没有空位;
  • 虚引用:"幽灵房"------ 房子已经快拆了,只能远远看着(跟踪回收),不能住。
关键应用:软引用实现缓存

比如浏览器后退功能:

  • 浏览过的网页用软引用缓存起来(SoftReference<WebPage>);
  • 内存足够时,后退能直接从缓存取,不用重新请求;
  • 内存不够时,GC 会回收这些缓存页面,避免 OOM------ 既兼顾了性能,又保证了内存安全。

三、对象 "死刑复核":finalize () 方法的最后机会

可达性分析判定为 "不可达" 的对象,不是立刻就被回收,而是进入 "缓刑期"------ 要经历两次标记,finalize () 是唯一的 "减刑机会"。

核心流程:
  1. 第一次标记:

    • 可达性分析后,对象不可达 → 第一次标记;
    • 筛选:判断对象是否 "有必要执行 finalize () 方法";
      • 没覆盖 finalize () 方法 → 直接判定为 "死刑",等待回收;
      • 覆盖了 finalize () 方法 → 暂时不回收,把对象放进一个叫 "F-Queue" 的队列,等待后续处理。
  2. 第二次标记:

    • JVM 会启动一个低优先级的线程,去执行 F-Queue 里对象的 finalize () 方法(注意:不一定能执行完,因为线程优先级低,可能被打断);
    • 这是对象 "自救" 的最后机会:如果在 finalize () 里,对象重新和引用链建立关联(比如把自己赋值给某个静态变量),那么第二次标记时,它会被移出 "待回收集合",继续活着;
    • 如果没自救,或者 finalize () 执行完还没关联 → 第二次标记后,正式判定为 "死刑",后续 GC 会回收它。
关键注意点:
  • finalize () 只能执行一次:一个对象的 finalize () 被 JVM 调用后,就算它再次被判定为不可达,JVM 也不会再执行它的 finalize ()------ 相当于 "上诉只有一次机会";
  • 不推荐用 finalize ():它的执行时机不确定(可能很久才执行),而且容易写错(比如没真正自救,还占着资源),现在基本被try-with-resourcesCleaner API 替代。
自救示例(理解即可,不要写):
java 复制代码
public class FinalizeTest {
    public static FinalizeTest saveMe; // 静态变量,属于GC Roots

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("执行finalize()方法");
        saveMe = this; // 自救:把当前对象赋值给静态变量,重新关联引用链
    }

    public static void main(String[] args) throws InterruptedException {
        saveMe = new FinalizeTest();

        // 第一次让对象不可达
        saveMe = null;
        System.gc(); // 触发GC
        Thread.sleep(500); // 等待低优先级线程执行finalize()
        if (saveMe != null) {
            System.out.println("对象自救成功"); // 会打印,因为finalize()里重新关联了
        } else {
            System.out.println("对象已被回收");
        }

        // 第二次让对象不可达
        saveMe = null;
        System.gc();
        Thread.sleep(500);
        if (saveMe != null) {
            System.out.println("对象自救成功");
        } else {
            System.out.println("对象已被回收"); // 会打印,因为finalize()只能执行一次
        }
    }
}

四、扩展:方法区的 "垃圾回收"------ 无用类的判定

我们之前聊的都是 "堆里的对象回收",但方法区(元空间)也会产生垃圾(比如无用的类),只是回收频率很低(因为类的生命周期通常很长)。

核心:什么是 "无用的类"?(3 个条件必须同时满足)

类不像对象那样 "容易死",要被回收必须满足以下 3 个条件(相当于 "类的棺材钉" 要钉 3 颗):

  1. 堆中没有该类的任何实例:比如User类,所有new User()创建的对象都被回收了,堆里找不到User实例;
  2. 加载该类的 ClassLoader 已经被回收:比如自定义的类加载器(不是 JVM 自带的 BootstrapClassLoader),它本身被回收了(没有引用指向它);
  3. 该类的Class对象没有被引用:比如不能通过反射访问它(Class.forName("com.test.User")都获取不到),也没有任何地方持有这个Class对象的引用。
注意:
  • 满足这 3 个条件,JVM可以选择回收这个类,不是 "必须回收"(比如 JVM 为了性能,可能暂时不回收);
  • 常见场景:动态生成类的框架(比如 Spring、MyBatis),会动态创建很多类,如果不及时回收,可能导致方法区溢出(OOM: Metaspace)。

五、总结:把所有知识点串起来

JVM 的垃圾回收流程可以简化为:

  1. 可达性分析算法(基于 GC Roots)判断对象是否可达 → 不可达的对象进入 "缓刑期";
  2. 结合4 种引用类型调整回收优先级(强引用不回收,软引用内存不够才回收,弱引用 GC 就回收);
  3. 对 "缓刑期" 对象进行两次标记,通过finalize() 给予最后自救机会 → 最终判定为垃圾;
  4. 堆里的垃圾对象被回收,方法区里满足 3 个条件的无用类也可能被回收。

核心记住 3 个关键点:

  • 垃圾判定的核心是 "可达性分析",不是 "引用计数";
  • GC Roots 是 "不可回收的根对象",包括本地变量、静态变量等;
  • 引用类型决定回收优先级,finalize () 是最后自救机会(但不推荐用)。
相关推荐
鲸沉梦落1 小时前
JVM类加载
java·jvm
EAIReport3 小时前
自动化报告生成产品内嵌OA/BI平台:解决传统报告痛点的技术方案
java·jvm·自动化
没有bug.的程序员11 小时前
Java 字节码:看懂 JVM 的“机器语言“
java·jvm·python·spring·微服务
白露与泡影11 小时前
2025年BAT面试题汇总:JVM+Spring+Dubbo+Redis+并发编程
jvm·spring·dubbo
-大头.11 小时前
深入理解 Java 内存区域与 JVM 运行机制
java·jvm
没有bug.的程序员11 小时前
JVM 整体架构:一套虚拟机的心脏与血管
java·jvm·spring boot·spring cloud·架构
IUGEI15 小时前
【后端开发笔记】JVM底层原理-垃圾回收篇
java·jvm·笔记·后端
Boop_wu17 小时前
[Java EE] 多线程编程初阶
java·jvm·算法