Java虚拟机:对象复活、引用强度与Stop-The-World 1. 对象的“生死判决”从可触及到不可触及在JVM的眼中一个对象从“创建”到“消亡”并不是一瞬间的。如何判定一个对象是垃圾从根节点GC Roots出发如果无法找到一条引用链到达该对象该对象就被宣告“死亡”。但是对象真的会乖乖等死吗有时候它还有一次“苟延残喘”的机会——对象复活。1.1 对象的三种“可触及性”状态根据根节点能否访问到对象JVM将对象分为以下三种状态可触及的 (Reachable)从根节点出发可以直达该对象。在正常情况下我们new出来的对象就处于这个状态不会被回收。可复活的 (Resurrectable)对象的所有引用都被释放即将被回收。但在回收之前如果该对象重写了finalize()方法它就有可能在finalize()方法中重新建立引用从而“复活”自己。不可触及的 (Unreachable)对象的finalize()方法已经被JVM调用过并且对象没有复活。此时对象彻底变为“不可触及”状态只能等待被回收。核心知识点finalize()方法只会被JVM调用一次。如果一个对象在finalize()中自救成功下一次再被判定为垃圾时JVM不会再调用其finalize()它将直接进入“不可触及”状态并死亡。1.2 实战利用finalize()方法复活对象看一段经典的“对象复活”代码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);。特点这是最“强势”的引用。只要强引用还存在垃圾回收器永远不会回收该对象。即使内存耗尽抛出OOMJVM也不会回收强引用对象。隐患容易造成内存泄漏。如长生命周期对象持有短生命周期对象的强引用。2.2 软引用 (SoftReference) —— “内存告急时的救星”定义使用java.lang.ref.SoftReference实现。特点软引用比强引用“弱”。如果内存空间充足垃圾回收器不会回收它如果内存空间不足在抛出OOM之前JVM会强制回收软引用所指向的对象。适用场景非常适合实现内存敏感的高速缓存。比如图片缓存、网页缓存等。内存够时就留着加速系统不够时自动清除防止内存溢出。示例代码User u new User(1, geym); SoftReferenceUser 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实现。这是最弱的一种引用。特点无法获取对象永远无法通过虚引用的get()方法获取真实的对象调用get()永远返回null。必须搭配引用队列虚引用必须与ReferenceQueue联合使用。用于追踪回收当垃圾回收器准备回收对象时如果发现该对象还有虚引用回收后会将该虚引用放入关联的引用队列中。应用程序可以通过监控这个队列得知对象被回收的确切时机。适用场景主要用于直接内存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 停顿我们来模拟一段大量消耗内存并打印时间的代码public class StopWorldTest { // 消费内存的线程 public static class MyThread extends Thread { HashMapLong, 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参数启动程序。假设我们在控制台看到的时间输出出现了“断层”15.690 15.791 15.992 16.397 16.498 -- 这里出现了停顿下一行是 17.123 (跨度达600多毫秒) 17.123 17.226这就说明在这个时间段内JVM进行了Full GC。查看gc.log日志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 secsGC总共耗时622毫秒正是这622毫秒的GC停顿导致了PrintThread的输出在16.498和17.123之间出现了断裂。3.4 如何缓解 STW 带来的影响GC停顿是不可能消除的我们只能通过调优降低它的影响程度调整新生代与老年代的比例从日志中发现新生代GC极其频繁。调大新生代空间如-Xmn900m可以减少GC触发频率虽然单次GC时间略微增加但整体累计停顿时间会大幅减少。选择垃圾收集器对于低延迟低STW时间要求的系统应使用CMS或G1垃圾收集器尽量将GC的停顿控制在可控的毫秒级别。合理设置堆大小不要使用过小的堆否则频繁触发Full GC会导致严重的性能雪崩。总结梳理 JVM 垃圾回收知识点对象生命周期经历过可触及-可复活finalize()-不可触及三个阶段。finalize()仅调用一次且不推荐使用。引用强度层级强引用绝不回收内存泄漏之源。软引用内存不足时回收适合做缓存。弱引用每次GC时回收避免ThreadLocal等内存泄漏。虚引用无法获取对象仅用于追踪GC过程。GC的影响垃圾回收会触发Stop-The-World暂停所有用户线程。调优GC的核心就是平衡吞吐量和低停顿时间通过分析GC日志可以精确定位性能瓶颈。