Finalizer线程拖垮GC吞吐量是实测下降20%+,因重写finalize()的对象被强制注册进单线程F-Queue,由低优先级Finalizer线程串行处理,任一阻塞(如sleep或锁等待)即卡住全队列,导致对象延迟两个GC周期才可回收,堆中大量对象被Finalizer引用链持有,引发内存压力与GC频次飙升。

Finalizer 线程如何拖垮 GC 吞吐量
不是“可能影响”,而是实测吞吐下降 20%+——只要对象重写了 finalize(),JVM 就必须把它注册进一个单线程队列(F-Queue),由低优先级的 Finalizer 线程串行处理。这意味着:哪怕只有一个对象的 finalize() 里执行了 Thread.sleep(1000) 或等待锁,整个队列都会卡住,后续所有待终结对象全部堆积在堆里,GC 不敢回收它们(因为 Finalizer 还没执行完,对象仍可能“复活”)。
- 每个带
finalize()的对象至少延迟两个 GC 周期才能真正释放:第一次标记入队、第二次 Finalizer 执行完才真正可回收 - 堆中大量对象被
java.lang.ref.Finalizer引用链持有,jmap -histo会看到Finalizer实例数持续上涨 - 高对象创建率场景(如短生命周期 DTO、缓存 entry)下,这种延迟直接转化为内存压力和 GC 频次飙升
为什么 JDK 9 标记 @Deprecated 之后还要在 JDK 18 彻底删除
标记弃用只是第一步,真正推动移除的是它已无法满足现代 JVM 的 GC 设计目标:吞吐、延迟、内存占用三者必须可权衡。而 finalize() 是唯一一个让三者全崩盘的机制——它强制引入不可控延迟(Finalizer 线程不可预测)、放大内存占用(对象滞留堆中更久)、且无法并行化(单线程队列本质是反扩展的)。
- JDK 9 起
Cleaner已作为标准替代:无继承要求、不阻塞 GC、清理逻辑与对象生命周期自动绑定 - JDK 18 移除不是“加强警告”,而是清除所有运行时支持:类文件校验器拒绝含
finalize()的类加载,javac编译直接报错(非仅警告) - 遗留代码若真依赖
finalize()清理 native 资源,必须改用PhantomReference + ReferenceQueue主动轮询,否则启动即失败
常见误用场景与对应修复方式
很多开发者以为“只在测试里调用 System.gc() 触发 finalize() 没问题”,但实际只要类定义了该方法,JVM 就全程启用 Finalizer 机制——无论你是否显式触发 GC。
-
误用:用
finalize()关闭文件/数据库连接 → 修复:实现AutoCloseable,用 try-with-resources -
误用:在
finalize()中打印日志或发监控 → 修复:改用Cleaner.register(obj, () -> log.info("cleaned")) -
误用:认为 “
Object.finalize()是兜底保障” → 修复:没有兜底——资源泄漏必须靠显式管理,Finalizer 从不保证执行
排查 finalize 相关 OOM 的关键信号
一旦应用出现 java.lang.OutOfMemoryError: Java heap space 且堆 dump 显示大量对象被 Finalizer 引用链持有,基本可锁定问题。注意:这不是 GC 算法问题,而是代码层引入了不可伸缩的终结机制。
- 检查
jstat -gc <pid></pid>输出中FGC(Full GC)次数异常升高,且每次耗时增长 - 用
jmap -finalizerinfo <pid></pid>查看当前 Finalizer 队列长度(JDK 7+ 支持),>100 即危险 - 反编译 class 文件,搜索
protected void finalize()—— 即使方法体为空,也已触发 JVM 特殊处理
最麻烦的不是写错,而是有人把 finalize() 当成“免费的资源清理钩子”加进基类,结果整条继承链上的所有子类都被动继承了这个性能炸弹。

















