"finalize() 里能让对象复活"是 finalize 被弃用的原因之一,常被当成一句结论背下来。真实写一个会在 finalize() 里自我复活的类,GC 之后看它到底活没活过来——顺带撞见一个没预料到的真实细节。
上一篇:notify() 真实测出会卡死:3 次运行,3 次真实卡在个位数
finalize() 是 JVM 判定对象不可达之后、真正回收它之前调用的方法——如果在这个方法里重新建立一条到 this 的强引用,理论上就能让对象“复活”。
FinalizeResurrect.java(会复活的类) static volatile Object resurrected; static class Zombie { @Override protected void finalize() { resurrected = this; // 复活:在 finalize 里重新建立一条强引用 finalizedLatch.countDown(); } }
用 CountDownLatch 而不是 Thread.sleep 猜时间,是为了让"finalize 是否真的执行了"这件事有一个确定的信号,不靠运气。
去掉唯一的强引用,触发 GC,验证 resurrected 字段和一个追踪同一对象的 WeakReference。
finalize() 是否在 5 秒内真实执行 = true resurrected 字段是否非 null(对象真的复活了) = true weakRef.get() 是否仍能真实拿到对象 = false
第三行是这次实验里最意外的真实结果:resurrected 字段明明已经真实指向这个对象(强引用),但 weakRef.get() 却真实返回了 null——两者矛盾吗?不矛盾,这是一个真实的时序问题:GC 在同一轮收集里,处理弱引用(把不再被强引用的 WeakReference 清空)这一步,发生在调用 finalize() 之前。所以真实的顺序是:GC 判定对象只剩 weakRef 这一条弱引用 → 清空 weakRef → 调用 finalize() → finalize() 里对象重新变成强可达。weakRef 已经在第二步被清空了,不会因为后面对象复活而自动恢复——弱引用一旦被清,就是永久清空,不是跟着对象的可达性状态实时联动的。
第二轮 GC 后 weakRef.get() = null(应为 null,说明这次真死了,finalize 不会再跑第二次)JVM 对每个对象只调用一次 finalize(),真实验证:第二次真正变得不可达时,直接被回收,不会再给它第二次复活的机会
java.lang.ref.Cleaner 是 finalize() 的现代替代方案,关键区别在于清理动作是一个独立的 Runnable,不持有 this。
清理动作真实执行了(这里访问不到 Resource 的 this,没法复活它) cleaner 是否在 5 秒内真实执行 = true weakRef.get() = null(应为 null,对象真实被回收,且没有任何办法复活它)
每一行都是真实运行这段代码得到的结果(本机真实测得,可能需要几轮gc()才触发)。