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 () 是最后自救机会(但不推荐用)。
相关推荐
jmxwzy10 小时前
JVM(java虚拟机)
jvm
Maỿbe11 小时前
JVM中的类加载&&Minor GC与Full GC
jvm
人道领域12 小时前
【零基础学java】(等待唤醒机制,线程池补充)
java·开发语言·jvm
小突突突12 小时前
浅谈JVM
jvm
饺子大魔王的男人13 小时前
远程调试总碰壁?局域网成 “绊脚石”?Remote JVM Debug与cpolar的合作让效率飙升
网络·jvm
天“码”行空1 天前
java面向对象的三大特性之一多态
java·开发语言·jvm
独自破碎E1 天前
JVM的内存区域是怎么划分的?
jvm
期待のcode1 天前
认识Java虚拟机
java·开发语言·jvm
leaves falling1 天前
一篇文章深入理解指针
jvm
linweidong1 天前
C++ 中避免悬挂引用的企业策略有哪些?
java·jvm·c++