Java的finalize居然还能这样坑我?

发布时间:2026/10/1 16:09:52
Java的finalize居然还能这样坑我? 上周排查一个线上GC问题发现服务频繁Full GC老年代堆积了大量本该被回收的对象。用MAT分析堆转储文件时我直接愣住了——这些对象的引用链居然全卡在Finalizer队列里。原来是被finalize()方法给坑了这玩意儿不是早就被标记为废弃了吗怎么还能在JDK 17的时代阴魂不散 如果你觉得finalize()只是性能差、调用时机不确定那今天这个坑可能让你重新认识它。1. 真实场景延迟释放的幽灵锁我们的订单系统有个OrderSession对象内部持有了一个分布式锁通过Redisson实现。按照设计这个锁应该在close()方法中显式释放但为了兜底某位同事在finalize()里加了一行lock.unlock()。上线后一切正常直到某天流量增长到3000 QPS突然出现大量锁等待超时。Redisson服务端显示这些锁明明没有被任何客户端持有但就是无法被其他线程获取。// 错误示范在finalize里释放锁 public class OrderSession implements AutoCloseable { private final RLock lock; Override public void close() { lock.unlock(); // 正确释放位置 } Override protected void finalize() throws Throwable { try { lock.unlock(); // 致命陷阱 } finally { super.finalize(); } } }2. 根因Finalizer线程的隐藏规则问题出在JVM的Finalizer线程工作机制上单线程串行处理所有对象的finalize()都在同一个系统级线程Finalizer线程执行且完全串行不可预测的延迟从对象不可达到finalize()被调用期间可能经历数次GC我们实测最长延迟达到12秒静默吞异常finalize()抛出的异常会被JVM直接吞掉连日志都不会留在我们的案例中Redisson的锁释放需要网络请求而Finalizer线程没有重试机制。一旦某次释放失败锁在Redis服务端已过期但本地OrderSession对象因为finalize()卡住迟迟无法回收其他线程获取新锁时误判为锁被占用Redisson的看门狗机制也救不了3. 对比测试有finalize vs 无finalize用JMH模拟创建100万个OrderSession对象分别测试显式close()和依赖finalize()的表现指标显式close()依赖finalize()对象回收耗时200ms8.4s内存峰值1.2GB3.5GB锁释放成功率100%82%关键结论finalize()让对象晋升到老年代的概率提高了4倍且网络操作失败率惊人。4. 避坑清单绝对不要用finalize管理资源特别是文件句柄、数据库连接、锁等需要确定释放的资源。连JDK自己的FileInputStream都在JDK 9改用Cleaner机制了。警惕第三方库的finalize有些老库比如早期的Xerces解析器会在内部用finalize()。如果发现对象堆积在Finalizer队列用jmap -histo:live pid | grep Finalizer揪出真凶。连AutoCloseable都别指望你以为实现了AutoCloseable就安全了试试这段代码try (OrderSession session new OrderSession()) { throw new RuntimeException(业务异常); } // 这里close()确实会被调用但如果close()本身也抛异常呢正确做法是双重保险try { try (OrderSession session new OrderSession()) { throw new RuntimeException(业务异常); } } catch (Exception e) { // 处理业务异常和close异常 }手动清除场景用PhantomReference确实需要对象死亡回调用Cleaner或PhantomReference它们至少不会阻塞GCpublic class SafeCleanup implements AutoCloseable { private final Cleaner cleaner; private final Resource resource; public SafeCleanup() { this.resource new Resource(); this.cleaner Cleaner.create(); cleaner.register(this, resource::cleanup); } Override public void close() { cleaner.clean(); } }5. 终极建议编译期封杀在pom.xml里加上这个插件让项目彻底禁止finalize()plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-enforcer-plugin/artifactId executions execution idban-finalize/id goals goalenforce/goal /goals configuration rules banDuplicateClasses findAllDuplicatestrue/findAllDuplicates /banDuplicateClasses bannedMethods methodjava.lang.Object finalize/method /bannedMethods /rules /configuration /execution /executions /plugin现在你还敢信任finalize()吗你们团队有没有更骚的操作来评论区聊聊那些年一起填过的坑。