Java虚拟机:对象复活、引用强度与Stop-The-World

1. 对象的"生死判决":从可触及到不可触及

在JVM的眼中,一个对象从"创建"到"消亡"并不是一瞬间的。如何判定一个对象是垃圾?从**根节点(GC Roots)**出发,如果无法找到一条引用链到达该对象,该对象就被宣告"死亡"。

但是,对象真的会乖乖等死吗?有时候,它还有一次"苟延残喘"的机会------对象复活

1.1 对象的三种"可触及性"状态

根据根节点能否访问到对象,JVM将对象分为以下三种状态:

  1. 可触及的 (Reachable) :从根节点出发,可以直达该对象。在正常情况下,我们new出来的对象就处于这个状态,不会被回收。

  2. 可复活的 (Resurrectable) :对象的所有引用都被释放,即将被回收 。但在回收之前,如果该对象重写了finalize()方法,它就有可能在finalize()方法中重新建立引用,从而"复活"自己。

  3. 不可触及的 (Unreachable) :对象的finalize()方法已经被JVM调用过,并且对象没有复活。此时对象彻底变为"不可触及"状态,只能等待被回收。

💡 核心知识点finalize()方法只会被JVM调用一次 。如果一个对象在finalize()中自救成功,下一次再被判定为垃圾时,JVM不会再调用其finalize(),它将直接进入"不可触及"状态并死亡。

1.2 实战:利用 finalize() 方法复活对象

看一段经典的"对象复活"代码:

java 复制代码
public class CanReliveObj {
    public static CanReliveObj obj;

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("CanReliveObj finalize called");
        obj = this; // 关键:重新建立引用,自己救自己!
    }

    public static void main(String[] args) throws InterruptedException {
        obj = new CanReliveObj();
        obj = null; // 1. 断开强引用

        System.gc(); // 2. 第一次GC
        Thread.sleep(1000); // 给GC线程一点时间执行finalize

        if (obj == null) {
            System.out.println("obj是null");
        } else {
            System.out.println("obj可用"); // 3. 对象成功复活
            System.out.println("第2次gc");
            obj = null; 
            System.gc(); // 4. 第二次GC
            Thread.sleep(1000);
            if (obj == null) {
                System.out.println("obj是null"); // 5. 这次真的没了
            } else {
                System.out.println("obj可用");
            }
        }
    }
}

运行结果分析

  • 第一次GCSystem.gc()触发回收,JVM检测到对象重写了finalize(),将其放入一个低优先级的队列中执行。在finalize()执行时,this指针将自身赋值给了静态变量obj,导致该对象再次被GC Roots引用,成功"复活"。

  • 第二次GC :再次置空并触发GC。此时JVM不再调用finalize()方法,对象没有机会自救,被彻底回收。

⚠️ 重要警告极其不推荐在实际开发中依赖finalize()来释放资源! 它不仅调用时机不确定,还会导致对象复活、内存泄漏等难以排查的诡异Bug。Java 9之后该方法已被标记为弃用(Deprecated)。


2. 引用的强度:强、软、弱、虚

既然对象是否被回收完全取决于"引用"的状态,Java在java.lang.ref包中提供了4种引用级别。除了我们最熟悉的强引用外,其他三种旨在帮助开发者更好地控制GC时机,常见于缓存、内存敏感型应用等场景。

2.1 强引用 (Strong Reference) ------ "死也不放手"

  • 定义 :我们在代码中直接new出来的对象引用。例如 StringBuffer str = new StringBuffer("hello");

  • 特点 :这是最"强势"的引用。只要强引用还存在,垃圾回收器永远不会 回收该对象。即使内存耗尽抛出OOM,JVM也不会回收强引用对象。

  • 隐患:容易造成内存泄漏。如长生命周期对象持有短生命周期对象的强引用。

2.2 软引用 (SoftReference) ------ "内存告急时的救星"

  • 定义 :使用java.lang.ref.SoftReference实现。

  • 特点 :软引用比强引用"弱"。如果内存空间充足 ,垃圾回收器不会 回收它;如果内存空间不足 ,在抛出OOM之前,JVM会强制回收软引用所指向的对象。

  • 适用场景 :非常适合实现内存敏感的高速缓存。比如图片缓存、网页缓存等。内存够时就留着加速系统,不够时自动清除防止内存溢出。

示例代码

java 复制代码
User u = new User(1, "geym");
SoftReference<User> userSoftRef = new SoftReference<>(u);
u = null; // 断开强引用,此时对象只有软引用

// 即使调用GC,对象通常依然存在
System.gc(); 
System.out.println(userSoftRef.get()); // 依然能输出对象

// 强制分配大量内存,触发内存紧张
byte[] b = new byte[1024 * 935 * 7]; 
System.gc(); 
System.out.println(userSoftRef.get()); // 输出 null,因为内存紧张被回收了

2.3 弱引用 (WeakReference) ------ "每一次GC都是生死劫"

  • 定义 :使用java.lang.ref.WeakReference实现。

  • 特点 :比软引用更弱。只要发生垃圾回收 (无论内存是否紧张),弱引用的对象一定会被回收。不过由于GC线程优先级较低,弱引用对象可能存活一小段时间。

  • 适用场景WeakHashMap、以及ThreadLocal中的ThreadLocalMap正是利用了弱引用来防止内存泄漏(当线程销毁时,ThreadLocal能够被及时回收)。

2.4 虚引用 (PhantomReference) ------ "影子般的跟踪者"

  • 定义 :使用java.lang.ref.PhantomReference实现。这是最弱的一种引用。

  • 特点

    1. 无法获取对象 :永远无法通过虚引用的get()方法获取真实的对象,调用get()永远返回null

    2. 必须搭配引用队列 :虚引用必须与ReferenceQueue联合使用。

    3. 用于追踪回收:当垃圾回收器准备回收对象时,如果发现该对象还有虚引用,回收后会将该虚引用放入关联的引用队列中。应用程序可以通过监控这个队列,得知对象被回收的确切时机。

  • 适用场景 :主要用于**直接内存(Direct Memory)**的回收跟踪,以及某些框架中实现细粒度的对象回收后清理操作(比如NIO中的回收清理)。


3. 垃圾回收的代价:Stop-The-World (STW)

理解了引用的原理,我们知道GC会回收垃圾。但GC是"免费"的吗?绝对不是。为了能够安全地标记和清除垃圾,JVM必须暂停所有用户线程,这就是著名的 Stop-The-World (STW) 现象。

3.1 为什么会有 STW?

就像我们在打扫一间有人的房间一样,人如果不停止走动(产生新的垃圾),我们是永远扫不干净的。GC也是如此,为了保证系统状态的一致性和垃圾的准确性,GC执行时必须暂停所有应用线程。STW期间,整个应用程序"卡死",没有任何响应,这是Java程序性能调优中最需要关注的点。

3.2 实战演示:压力下暴露的 GC 停顿

我们来模拟一段大量消耗内存并打印时间的代码:

java 复制代码
public class StopWorldTest {
    // 消费内存的线程
    public static class MyThread extends Thread {
        HashMap<Long, byte[]> map = new HashMap<>();
        @Override
        public void run() {
            try {
                while (true) {
                    // 内存占满时清理
                    if (map.size() * 512 / 1024 / 1024 >= 900) {
                        map.clear();
                        System.out.println("clean map");
                    }
                    for (int i = 0; i < 100; i++) {
                        map.put(System.nanoTime(), new byte[512]);
                    }
                }
            } catch (Exception e) {}
        }
    }
    
    // 定时打印时间的线程
    public static class PrintThread extends Thread {
        public static final long starttime = System.currentTimeMillis();
        @Override
        public void run() {
            try {
                while (true) {
                    long t = System.currentTimeMillis() - starttime;
                    System.out.println(t / 1000 + "." + t % 1000); // 打印时间戳
                    Thread.sleep(100); // 每100ms打印一次
                }
            } catch (Exception e) {}
        }
    }
}

3.3 GC 日志中的真相

我们使用 -Xmx1g -Xms1g -Xmn900m -XX:+UseSerialGC -Xloggc:gc.log -XX:+PrintGCDetails 参数启动程序。

假设我们在控制台看到的时间输出出现了"断层":

java 复制代码
15.690
15.791
15.992
16.397
16.498  <-- 这里出现了停顿,下一行是 17.123 (跨度达600多毫秒)
17.123
17.226

这就说明在这个时间段内,JVM进行了 Full GC。查看 gc.log 日志:

java 复制代码
16.643: [GC (Allocation Failure) 16.650: [DefNew (promotion failed) : 478630K->468622K(614400K), 0.3180471 secs] 16.968: [Tenured: 126975K->126975K(126976K), 0.2966344 secs] 478630K->330798K(741376K), 0.6221686 secs] 

日志解析

  • 16.643:这是JVM启动到发生GC的耗时。

  • 0.6221686 secs :GC总共耗时 622毫秒

  • 正是这622毫秒的GC停顿,导致了PrintThread的输出在 16.49817.123 之间出现了断裂。

3.4 如何缓解 STW 带来的影响?

GC停顿是不可能消除的,我们只能通过调优降低它的影响程度:

  1. 调整新生代与老年代的比例 :从日志中发现,新生代GC极其频繁。调大新生代空间(如 -Xmn900m),可以减少GC触发频率,虽然单次GC时间略微增加,但整体累计停顿时间会大幅减少

  2. 选择垃圾收集器 :对于低延迟(低STW时间)要求的系统,应使用 CMSG1 垃圾收集器,尽量将GC的停顿控制在可控的毫秒级别。

  3. 合理设置堆大小:不要使用过小的堆,否则频繁触发Full GC会导致严重的性能雪崩。


总结:梳理 JVM 垃圾回收知识点

  1. 对象生命周期 :经历过可触及 -> 可复活finalize())-> 不可触及 三个阶段。finalize()仅调用一次且不推荐使用。

  2. 引用强度层级

    • 强引用:绝不回收,内存泄漏之源。

    • 软引用:内存不足时回收,适合做缓存。

    • 弱引用:每次GC时回收,避免ThreadLocal等内存泄漏。

    • 虚引用:无法获取对象,仅用于追踪GC过程。

  3. GC的影响 :垃圾回收会触发 Stop-The-World 暂停所有用户线程。调优GC的核心就是平衡吞吐量低停顿时间,通过分析GC日志可以精确定位性能瓶颈。

相关推荐
布朗克1681 小时前
Go 入门到精通-33-unsafe 与 CGO
开发语言·后端·golang·unsafe·cgo
凤山老林1 小时前
SpringBoot实战:构建优雅的全局异常处理机制
java·springboot·异常处理
157092511342 小时前
【无标题】
开发语言·python·算法
小Ti客栈2 小时前
Spring Boot 整合 Swagger2 和 Knife4j实现接口文档与可视化调试
java·spring boot·后端
梅头脑2 小时前
ThreadLocalMap里几十万个死Entry——排查了半天OOM,根因就一行finally没写
java
xieliyu.2 小时前
MySQL 存储过程详解:概念、创建与删除全教程
开发语言·数据库·mysql
2zcode2 小时前
项目文档:基于MATLAB最小二乘支持向量机的睡眠分期智能监测系统设计与实现
开发语言·支持向量机·matlab
twcc_come2 小时前
安全策略配置实验报告
开发语言·php
2601_963870172 小时前
【计算机毕业设计】基于Spring Boot的社区老年大学课程报名与学习系统的设计与实现
java·spring boot·学习