上周排查一个线上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() |
|---|---|---|
| 对象回收耗时 | 200ms | 8.4s |
| 内存峰值 | 1.2GB | 3.5GB |
| 锁释放成功率 | 100% | 82% |
- 关键结论:
finalize()让对象晋升到老年代的概率提高了4倍,且网络操作失败率惊人。
4. 避坑清单
特别是文件句柄、数据库连接、锁等需要
确定释放的资源。连JDK自己的FileInputStream都在JDK 9改用Cleaner机制了。有些老库(比如早期的Xerces解析器)会在内部用finalize()。如果发现对象堆积在Finalizer队列,用jmap -histo:live pid | grep Finalizer揪出真凶。
你以为实现了AutoCloseable就安全了?试试这段代码:
try (OrderSession session = new OrderSession()) { throw new RuntimeException("业务异常"); } // 这里close()确实会被调用,但如果close()本身也抛异常呢?正确做法是
双重保险:try { try (OrderSession session = new OrderSession()) { throw new RuntimeException("业务异常"); } } catch (Exception e) { // 处理业务异常和close异常 }确实需要"对象死亡"回调?用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()吗?你们团队有没有更骚的操作?来评论区聊聊那些年一起填过的坑。