Finalizer线程串行处理待终结对象,任一finalize()执行慢都会阻塞后续对象回收,导致堆内存持续增长和OOM;JDK 9起弃用、JDK 18彻底移除,应改用Cleaner或PhantomReference。

Finalizer线程会阻塞对象回收,导致堆内存持续增长
Java 的 finalize() 方法不是“析构函数”,它不保证执行时机,也不保证一定执行。真正危险的是:JVM 用一个单线程——Finalizer 线程——串行处理所有待终结对象。只要有一个 finalize() 执行慢(比如 IO、锁等待、死循环),后续所有对象都会卡在队列里等,GC 无法回收它们的内存。
常见错误现象:java.lang.OutOfMemoryError: Java heap space,但 jmap 查看堆里大量对象是 java.lang.ref.Finalizer 引用链持有,且 Finalizer 队列长度不断上涨。
- 使用场景极少:仅在极旧 JDK(如 6/7)遗留代码或某些 NIO DirectBuffer 的兜底清理中见过,现代代码应完全规避
- 性能影响极大:每个
finalize()调用至少带来两次 GC 周期延迟(入队 + 实际执行),实测吞吐下降 20%+(尤其高创建率对象) - JDK 9 开始标记为
@Deprecated(since="9"),JDK 18 彻底移除 —— 不是“建议不用”,是“已废止”
替代 finalize() 的标准方案只有 Cleaner 和 PhantomReference
Cleaner 是 JDK 9 引入的轻量级、无栈、非继承式清理机制;PhantomReference 则提供更底层的、需手动管理的虚引用队列。二者都不依赖 Finalizer 线程,不会阻塞 GC。
关键区别:Cleaner 自动绑定到对象生命周期,无需子类化;PhantomReference 必须配合 ReferenceQueue 主动轮询,适合需要精确控制清理时机的场景(如 native 资源)。
立即学习“Java免费学习笔记(深入)”;
- 不要继承
Object重写finalize()—— 这是触发 Finalizer 队列的唯一入口,也是风险源头 -
Cleaner示例:private static final Cleaner cleaner = Cleaner.create();<br>private final Cleanable cleanable = cleaner.register(this, () -> releaseNativeResource());
-
PhantomReference必须配合ReferenceQueue使用,且不能通过它访问被引用对象(否则阻止回收)
已有 finalize() 的老代码怎么安全下线
直接删掉 finalize() 方法大概率引发资源泄漏,尤其涉及文件句柄、socket、DirectByteBuffer 等。必须先确认资源是否已被其他方式显式释放。
排查重点不是“有没有 finalize()”,而是“哪些资源没被 close()/free()”。
- 用
jstack -l <pid>检查是否存在Finalizer线程长时间 RUNNABLE 或 BLOCKED - 用
jmap -histo:live <pid>观察java.lang.ref.Finalizer实例数是否随时间上升 - 对每个含
finalize()的类,补全 try-with-resources 或显式close()调用,并在 finally 块中置空关键字段(防止 finalize 再次触发) - 上线前加 JVM 参数
-XX:+ReportExceptionOnFailedFinalization,让未捕获异常直接 crash,暴露隐藏问题
Finalizer 相关的 JVM 参数几乎都该禁用
-XX:+FinalizerTimeout、-XX:FinalizerThreshold 等参数看似可调优,实则治标不治本。它们只是延长或缩短 Finalizer 线程的等待时间,无法解决串行瓶颈和不确定性。
真正有效的配置只有一条:-XX:+DisableExplicitGC(防 System.gc() 误触发 Finalizer 队列积压),以及 JDK 9+ 下默认启用的 Cleaner 机制。
-
-XX:+PrintGCDetails中若出现Finalizer相关日志(如 “Finalizer” in GC log),说明已在走高危路径 - 禁止使用
System.runFinalizersOnExit(true)—— 它已被废弃且极度危险,JDK 8 起抛UnsupportedOperationException - JDK 14+ 默认开启 ZGC/Shenandoah 时,
Finalizer线程行为更不可预测,务必提前清理
Finalizer 线程的“慢”不是性能问题,是设计缺陷;它的存在本身就意味着资源管理失控。真正难的不是替换语法,而是把隐式依赖终结器的资源生命周期,显式地、确定性地收归到业务逻辑中——这点,连很多资深开发者都容易忽略。







