1. 引言

本文将探讨 Java 18 通过 JEP 421 提案弃用 Object 终结机制(finalization)的背景,并分析其替代方案。终结机制自 Java 9 起已被标记为废弃,Java 18 进一步推进了移除计划。我们将深入探讨终结机制的缺陷及现代替代方案。

2. Java 中的终结机制

2.1 资源泄漏问题

JVM 的垃圾回收(GC)机制会回收应用程序不再使用的对象内存,但某些对象引用关联着操作系统资源(如文件描述符、原生内存块)。这些对象需要显式调用 close() 方法释放资源。

如果 GC 在对象调用 close() 前就回收了对象,操作系统仍认为资源被占用,导致资源泄漏。典型场景是文件操作:

public void copyFileOperation() throws IOException {
    try {
        fis = new FileInputStream("input.txt");
        // 执行文件操作
        fis.close();
    } finally {
        if (fis != null) {
            fis.close(); // 即使 finally 块也可能抛异常导致泄漏
        }
    }
}

2.2 Object 的 finalize() 方法

Java 引入终结机制解决资源泄漏问题。**finalize() 方法(终结器)是 Object 类的 protected void 方法,用于释放对象占用的资源**。通过重写该方法实现资源清理:

public class MyFinalizableResourceClass {
    FileInputStream fis = null;

    public MyFinalizableResourceClass() throws FileNotFoundException {
        this.fis = new FileInputStream("file.txt");
    }

    public int getByteLength() throws IOException {
        return this.fis.readAllBytes().length;
    }

    @Override
    protected void finalize() throws Throwable {
        fis.close(); // 终结器中释放资源
    }
}

当对象可被回收时,GC 会调用其终结器。但终结机制本身存在根本性缺陷,自 Java 9 起已被废弃。

3. 终结机制的缺陷

3.1 执行不可预测

即使对象可被回收,finalize() 也未必被执行
GC 调用终结器的延迟不可控

终结器由 GC 调度执行,但 GC 触发时机取决于内存压力。若内存充足,GC 可能暂停运行,导致大量对象堆积在堆中等待终结,引发资源短缺。

3.2 终结器代码不受控

⚠️ 开发者可在终结器中执行任意代码
⚠️ 恶意代码可注入终结器破坏应用

即使父类省略终结器,子类仍可重写 finalize() 访问未完全初始化的对象,或通过反序列化攻击注入恶意代码。

3.3 性能开销

GC 需额外追踪含终结器的类
对象生命周期增加额外步骤

对于吞吐量优先的垃圾回收器(如 G1),终结器会增加 GC 暂停时间。且终结器始终启用,即使资源已手动关闭仍会执行,造成不必要的性能损耗。

3.4 线程无保障

JVM 不保证终结器的执行线程
不保证多个终结器的执行顺序

若应用线程分配资源的速度超过终结线程释放资源的速度,将导致资源耗尽。

3.5 终结器代码正确性难保证

⚠️ 必须显式调用 super.finalize()
⚠️ 多线程环境易引发死锁

终结器在未知线程上运行,可能引发多线程问题。多个含终结器的类会增加系统耦合性,导致对象因依赖关系长时间滞留堆中。

4. 替代方案:try-with-resources

Java 7 引入的 try-with-resources 结构可确保资源 close() 方法被调用,是终结机制的理想替代:

public void readFileOperationWithTryWith() throws IOException {
    try (FileOutputStream fis = new FileOutputStream("input.txt")) {
        // 执行操作
    } // 自动调用 close()
}

改造资源类实现 AutoCloseable 接口:

public class MyCloseableResourceClass implements AutoCloseable {
    private FileInputStream fis;
    
    public MyCloseableResourceClass() throws FileNotFoundException {
        this.fis = new FileInputStream("file.txt");
    }
    
    public int getByteLength() throws IOException {
        return this.fis.readAllBytes().length;
    }
    
    @Override
    public void close() throws IOException {
        this.fis.close();
    }
}

使用方式:

@Test
public void givenCloseableResource_whenUsingTryWith_thenShouldClose() throws IOException {
    int length = 0;
    try (MyCloseableResourceClass mcr = new MyCloseableResourceClass()) {
        length = mcr.getByteLength();
    }
    Assert.assertEquals(20, length);
}

5. Java 的 Cleaner API

5.1 使用 Cleaner API 创建资源类

Java 9 引入 Cleaner API 管理长生命周期资源。Cleaner 实现 Cleanable 接口,允许注册清理动作

public class MyCleanerResourceClass implements AutoCloseable {
    private static Resource resource;
    private static final Cleaner cleaner = Cleaner.create();
    private final Cleaner.Cleanable cleanable;

    public MyCleanerResourceClass() {
        resource = new Resource();
        this.cleanable = cleaner.register(this, new CleaningState());
    }

    @Override
    public void close() {
        this.cleanable.clean(); // 触发清理
    }

    static class CleaningState implements Runnable {
        @Override
        public void run() {
            System.out.println("Cleanup done"); // 清理逻辑
        }
    }
}

关键步骤:

  1. 获取 Cleaner 实例:Cleaner.create()
  2. 注册清理动作:cleaner.register(object, action)
  3. 执行清理:cleanable.clean()

5.2 测试 Cleaner 实现

@Test
public void givenMyCleanerResource_whenUsingCleanerAPI_thenShouldClean() {
    assertDoesNotThrow(() -> {
        try (MyCleanerResourceClass myCleanerResourceClass = new MyCleanerResourceClass()) {
            myCleanerResourceClass.useResource();
        }
    });
}

输出:

Using the resource
Cleanup done

5.3 Cleaner API 的优势

避免对象复活CleaningState 无法访问原始对象
防止未初始化对象:清理动作在对象构造完成后注册
可取消清理:通过 clean() 方法主动触发
线程隔离:清理动作在独立线程执行,异常被 JVM 忽略

6. 结论

Java 弃用终结机制是必然选择,其缺陷(不可预测性、性能开销、线程不安全)已不适应现代开发需求。推荐替代方案:

  1. **优先使用 try-with-resources**:简洁可靠,适用于短生命周期资源
  2. Cleaner API:适用于需要延迟清理的长生命周期资源

源代码可在 GitHub 获取。告别终结机制,拥抱更健壮的资源管理方式!


原始标题:Deprecate Finalization in Java 18