☰
Java的finalize居然还能这样坑我?
2026/10/1 16:09:51 网站建设 项目流程

上周排查一个线上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线程工作机制上:

  1. 单线程串行处理:所有对象的finalize()都在同一个系统级线程(Finalizer线程)执行,且完全串行
  2. 不可预测的延迟:从对象不可达,到finalize()被调用,期间可能经历数次GC(我们实测最长延迟达到12秒)
  3. 静默吞异常: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,它们至少不会阻塞GC:

          public 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> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-enforcer-plugin</artifactId> <executions> <execution> <id>ban-finalize</id> <goals> <goal>enforce</goal> </goals> <configuration> <rules> <banDuplicateClasses> <findAllDuplicates>true</findAllDuplicates> </banDuplicateClasses> <bannedMethods> <method>java.lang.Object finalize</method> </bannedMethods> </rules> </configuration> </execution> </executions> </plugin>

          现在,你还敢信任finalize()吗?你们团队有没有更骚的操作?来评论区聊聊那些年一起填过的坑。

          需要专业的网站建设服务?

          联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

          立即咨询