finalize()方法详解

3 阅读4分钟

finalize() 方法详解

finalize()java.lang.Object 中定义的一个 protected 方法,用于在对象被垃圾回收之前执行清理逻辑。它的设计初衷是提供一种与 C++ 析构函数类似的资源释放机制,但实际使用中存在诸多缺陷,在 Java 9 中已被标记为废弃


一、基本定义与调用时机

public class Object {
    protected void finalize() throws Throwable { }
}
  • 调用时机:当垃圾回收器确定对象不可达(没有任何强引用指向它)且将要回收其内存时,会调用该对象的 finalize() 方法。
  • 最多调用一次:JVM 保证每个对象的 finalize() 最多被执行一次。如果对象在 finalize() 中“复活”(重新被强引用),下次再次变成不可达时,不会再调用 finalize()
  • 异常行为:如果 finalize() 抛出未捕获的异常,该异常会被忽略,并且对象的终结会停止。

二、工作流程(JVM 规范)

  1. 可达性变化:对象变为“不可达”状态(无 GC Roots 路径)。
  2. 标记与排队:JVM 会检测到该对象覆盖了 finalize() 且尚未执行过终结,将其放入一个称为 Finalizer 的队列中。
  3. 独立线程处理:一个低优先级的守护线程 Finalizer 会从队列中取出对象,并调用其 finalize() 方法。
  4. 内存回收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.CleanerPhantomReference 代替。
  • 替代方案
    1. AutoCloseable + try-with-resources(显式释放)
      适用于可管理的资源(文件流、数据库连接等)。最推荐的方案。
    2. Cleaner(Java 9+)
      继承自 PhantomReference,可以注册清理任务,由 JVM 后台线程自动执行。DirectByteBuffer 的堆外内存释放就是用此机制。
    3. 自定义 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 中,FileInputStreamFileOutputStream 等类使用 finalize() 作为最后的安全网(确保文件描述符被关闭)。
  • 从 JDK 7 开始,这些类已经改用 CleanerPhantomReference 实现。
  • 如今 JDK 内部几乎不再使用 finalize()(除了极少数遗留类)。

七、总结

finalize() 是一个充满缺陷的、过时的资源清理机制,永远不应该在新代码中使用。
对于需要自动清理的资源,应优先实现 AutoCloseable 并配合 try-with-resources 显式释放;对于无法显式管理的本地内存(如 DirectByteBuffer),应使用 CleanerPhantomReference
如果你正在维护的老代码中仍有 finalize(),请尽快重构,以避免不可预测的性能和内存问题。