finalize() 方法详解
finalize() 是 java.lang.Object 中定义的一个 protected 方法,用于在对象被垃圾回收之前执行清理逻辑。它的设计初衷是提供一种与 C++ 析构函数类似的资源释放机制,但实际使用中存在诸多缺陷,在 Java 9 中已被标记为废弃。
一、基本定义与调用时机
public class Object {
protected void finalize() throws Throwable { }
}
- 调用时机:当垃圾回收器确定对象不可达(没有任何强引用指向它)且将要回收其内存时,会调用该对象的
finalize()方法。 - 最多调用一次:JVM 保证每个对象的
finalize()最多被执行一次。如果对象在finalize()中“复活”(重新被强引用),下次再次变成不可达时,不会再调用finalize()。 - 异常行为:如果
finalize()抛出未捕获的异常,该异常会被忽略,并且对象的终结会停止。
二、工作流程(JVM 规范)
- 可达性变化:对象变为“不可达”状态(无 GC Roots 路径)。
- 标记与排队:JVM 会检测到该对象覆盖了
finalize()且尚未执行过终结,将其放入一个称为Finalizer的队列中。 - 独立线程处理:一个低优先级的守护线程
Finalizer会从队列中取出对象,并调用其finalize()方法。 - 内存回收:
finalize()执行完毕后,对象变为“已终结”状态。下一次 GC 来临时,对象的内存才会被真正回收(如果对象没有复活)。
注意:从对象变成不可达到其
finalize()真正被调用,中间存在不确定的延迟(取决于 GC 调度和Finalizer线程的执行)。
三、典型使用场景(已不推荐)
- 释放 Native 资源:例如通过 JNI 分配的堆外内存、文件句柄等。
- 关闭外部连接:数据库连接、网络套接字等。
- 对象清理日志:调试时记录对象被回收的事件。
示例:
public class ResourceHolder {
private long nativePtr;
static {
System.loadLibrary("myNativeLib");
}
private native void allocate();
private native void free();
public ResourceHolder() {
allocate();
}
@Override
protected void finalize() throws Throwable {
try {
free(); // 释放 native 内存
} finally {
super.finalize();
}
}
}
四、严重缺陷(为什么它不好)
| 缺陷 | 说明 |
|---|---|
| 调用时机不可预测 | 从对象变成垃圾到 finalize() 执行可能经历多次 GC,甚至永远不执行(如果程序一直不触发 GC 或提前退出)。 |
| 性能极差 | 覆盖 finalize() 的对象在 GC 时需要额外处理:至少经历两次 GC 才能回收(第一次放进队列,第二次才真正释放内存)。大量使用会严重拖慢 GC 吞吐量。 |
| 可能“复活”对象 | 在 finalize() 中,如果让 this 被某个强引用持有(例如赋值给静态变量),对象就复活了,导致资源管理混乱。 |
| 异常被吞没 | 若 finalize() 抛出异常,异常被忽略,对象终结过程停止,资源可能永远无法释放。 |
| 顺序不确定 | 对象间若有依赖关系(如 A 持有 B 的引用),finalize() 的执行顺序无法保证,可能先终结 A 再使用 A 的资源。 |
| 与 GC 耦合 | finalize() 的存在迫使 JVM 在回收内存前先执行终结逻辑,增加了垃圾回收器的复杂性。 |
五、官方废弃与替代方案
- Java 9+:
finalize()已被标记为@Deprecated(since="9", forRemoval=true),并建议使用java.lang.ref.Cleaner或PhantomReference代替。 - 替代方案:
AutoCloseable+try-with-resources(显式释放)
适用于可管理的资源(文件流、数据库连接等)。最推荐的方案。Cleaner(Java 9+)
继承自PhantomReference,可以注册清理任务,由 JVM 后台线程自动执行。DirectByteBuffer的堆外内存释放就是用此机制。- 自定义
PhantomReference+ReferenceQueue
更底层,完全控制清理时机和线程。
示例:使用 Cleaner(推荐)
public class Resource implements AutoCloseable {
private static final Cleaner cleaner = Cleaner.create();
private final Cleaner.Cleanable cleanable;
private long nativePtr;
public Resource() {
this.nativePtr = allocate();
this.cleanable = cleaner.register(this, () -> free(nativePtr));
}
@Override
public void close() {
cleanable.clean(); // 显式释放
}
private static native long allocate();
private static native void free(long ptr);
}
六、finalize() 在 JDK 内部的实际使用
- 在早期 JDK 中,
FileInputStream、FileOutputStream等类使用finalize()作为最后的安全网(确保文件描述符被关闭)。 - 从 JDK 7 开始,这些类已经改用
Cleaner或PhantomReference实现。 - 如今 JDK 内部几乎不再使用
finalize()(除了极少数遗留类)。
七、总结
finalize()是一个充满缺陷的、过时的资源清理机制,永远不应该在新代码中使用。
对于需要自动清理的资源,应优先实现AutoCloseable并配合try-with-resources显式释放;对于无法显式管理的本地内存(如DirectByteBuffer),应使用Cleaner或PhantomReference。
如果你正在维护的老代码中仍有finalize(),请尽快重构,以避免不可预测的性能和内存问题。