1. 概述

本教程聚焦Java语言的核心机制——由根Object类提供的finalize方法。

简单来说,该方法在特定对象被垃圾回收前调用。

2. 使用终结方法

finalize() 方法被称为终结方法。

当JVM判定特定实例应被垃圾回收时,会调用终结方法。此类终结方法可执行任意操作,甚至复活对象。

终结方法的主要目的是在对象从内存移除前释放其占用的资源。它既可作为清理操作的主要机制,也可作为其他方法失败时的安全网。

要理解终结方法的工作原理,看以下类声明:

public class Finalizable {
    private BufferedReader reader;

    public Finalizable() {
        InputStream input = this.getClass()
          .getClassLoader()
          .getResourceAsStream("file.txt");
        this.reader = new BufferedReader(new InputStreamReader(input));
    }

    public String readFirstLine() throws IOException {
        String firstLine = reader.readLine();
        return firstLine;
    }

    // 其他类成员
}

Finalizable类包含一个引用可关闭资源的reader字段。当创建该类对象时,会构造一个从类路径文件读取的BufferedReader实例。

该实例在readFirstLine方法中用于提取文件首行。注意示例代码中未关闭reader。

可通过终结方法实现关闭:

@Override
public void finalize() {
    try {
        reader.close();
        System.out.println("Closed BufferedReader in the finalizer");
    } catch (IOException e) {
        // ...
    }
}

终结方法的声明与普通实例方法无异。

实际上,垃圾回收器调用终结方法的时间取决于JVM实现和系统条件,这些我们都无法控制

为立即触发垃圾回收,我们利用System.gc方法。但在实际系统中,切勿显式调用该方法,原因如下:

  1. ⚠️ 性能开销大
  2. ⚠️ 不会立即触发垃圾回收——仅是提示JVM启动GC
  3. ✅ JVM更清楚何时需要调用GC

若需强制GC,可使用jconsole工具。

以下测试用例演示终结方法的运作:

@Test
public void whenGC_thenFinalizerExecuted() throws IOException {
    String firstLine = new Finalizable().readFirstLine();
    assertEquals("baeldung.com", firstLine);
    System.gc();
}

首行语句创建Finalizable对象并调用其readFirstLine方法。该对象未赋值给任何变量,故在调用System.gc时符合垃圾回收条件。

测试中的断言验证输入文件内容,仅用于证明自定义类按预期工作。

运行测试后,控制台将打印缓冲读取器在终结方法中关闭的消息。这表明finalize方法被调用并清理了资源。

至此,终结方法看似是销毁前操作的理想选择。但事实并非如此。

下一节将说明为何应避免使用终结方法。

3. 避免使用终结方法

尽管终结方法带来便利,但存在诸多缺陷。

3.1. 终结方法的缺陷

终结方法在执行关键操作时面临以下明显问题:

首先是缺乏及时性。由于垃圾回收随时可能发生,我们无法预知终结方法的执行时间。

单看这点问题不大,因为终结方法迟早会执行。但系统资源并非无限。因此可能在清理完成前耗尽资源,导致系统崩溃。

终结方法还影响程序可移植性。因垃圾回收算法依赖JVM实现,同一程序在不同系统上可能表现迥异。

性能开销是另一重大缺陷。具体而言,JVM在构造和销毁包含非空终结方法的对象时需执行更多操作

最后需讨论的是终结期间缺乏异常处理若终结方法抛出异常,终结过程将终止,使对象处于损坏状态且无任何通知。

3.2. 终结方法效应演示

暂且搁置理论,通过实践观察终结方法的影响。

定义一个含非空终结方法的新类:

public class CrashedFinalizable {
    public static void main(String[] args) throws ReflectiveOperationException {
        for (int i = 0; ; i++) {
            new CrashedFinalizable();
            // 其他代码
        }
    }

    @Override
    protected void finalize() {
        System.out.print("");
    }
}

注意*finalize()*方法——仅向控制台输出空字符串。若该方法完全为空,JVM会视该对象无终结方法。因此需提供几乎不执行任何操作的实现。

main方法中,for循环每次迭代创建一个新的CrashedFinalizable实例。该实例未赋值给任何变量,故符合垃圾回收条件。

在标记为*// 其他代码*处添加语句,查看运行时内存中的对象数量:

if ((i % 1_000_000) == 0) {
    Class<?> finalizerClass = Class.forName("java.lang.ref.Finalizer");
    Field queueStaticField = finalizerClass.getDeclaredField("queue");
    queueStaticField.setAccessible(true);
    ReferenceQueue<Object> referenceQueue = (ReferenceQueue) queueStaticField.get(null);

    Field queueLengthField = ReferenceQueue.class.getDeclaredField("queueLength");
    queueLengthField.setAccessible(true);
    long queueLength = (long) queueLengthField.get(referenceQueue);
    System.out.format("There are %d references in the queue%n", queueLength);
}

这些语句访问JVM内部类的字段,每百万次迭代后打印对象引用数。

通过执行main方法启动程序。我们可能预期它会无限运行,但事实并非如此。几分钟后,系统将因类似错误崩溃:

...
There are 21914844 references in the queue
There are 22858923 references in the queue
There are 24202629 references in the queue
There are 24621725 references in the queue
There are 25410983 references in the queue
There are 26231621 references in the queue
There are 26975913 references in the queue
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
    at java.lang.ref.Finalizer.register(Finalizer.java:91)
    at java.lang.Object.<init>(Object.java:37)
    at com.baeldung.finalize.CrashedFinalizable.<init>(CrashedFinalizable.java:6)
    at com.baeldung.finalize.CrashedFinalizable.main(CrashedFinalizable.java:9)

Process finished with exit code 1

垃圾回收器似乎未尽职尽责——对象数量持续增加直至系统崩溃。

若移除终结方法,引用数通常为0,程序将永久运行。

3.3. 原理分析

要理解垃圾回收器为何未按预期丢弃对象,需探究JVM内部工作机制。

当创建含终结方法的对象(称为引用对象)时,JVM会创建一个类型为java.lang.ref.Finalizer的伴随引用对象。引用对象准备垃圾回收后,JVM将其标记为待处理并放入引用队列。

可通过java.lang.ref.Finalizer类的静态字段queue访问该队列。

同时,名为Finalizer的特殊守护线程持续运行,在引用队列中查找对象。找到对象后,它从队列移除引用对象并对引用对象调用终结方法。

在下一垃圾回收周期,引用对象将被丢弃——当它不再被引用对象引用时。

若线程高速持续创建对象(如本例所示),Finalizer线程将无法跟上节奏。最终内存无法存储所有对象,导致OutOfMemoryError

注意本节展示的高速创建对象场景在现实中并不常见,但它说明了一个重要观点——终结方法代价极高。

4. 无终结方法示例

探索一个提供相同功能但不使用*finalize()*方法的解决方案。需注意以下示例并非替换终结方法的唯一方案。

它仅用于说明关键点:总有替代方案可避免使用终结方法。

新类声明如下:

public class CloseableResource implements AutoCloseable {
    private BufferedReader reader;

    public CloseableResource() {
        InputStream input = this.getClass()
          .getClassLoader()
          .getResourceAsStream("file.txt");
        reader = new BufferedReader(new InputStreamReader(input));
    }

    public String readFirstLine() throws IOException {
        String firstLine = reader.readLine();
        return firstLine;
    }

    @Override
    public void close() {
        try {
            reader.close();
            System.out.println("Closed BufferedReader in the close method");
        } catch (IOException e) {
            // 处理异常
        }
    }
}

CloseableResource类与原Finalizable类的唯一区别是实现AutoCloseable接口而非定义终结方法。

注意CloseableResourceclose方法体与Finalizable类终结方法体几乎相同。

以下测试方法读取输入文件并在任务完成后释放资源:

@Test
public void whenTryWResourcesExits_thenResourceClosed() throws IOException {
    try (CloseableResource resource = new CloseableResource()) {
        String firstLine = resource.readFirstLine();
        assertEquals("baeldung.com", firstLine);
    }
}

上述测试中,CloseableResource实例在try-with-resources语句的try块中创建,故当try-with-resources块执行完毕时资源自动关闭。

运行测试方法,将看到CloseableResource类的close方法输出消息。

5. 结论

本教程聚焦Java核心概念——finalize方法。它看似有用,但在运行时可能产生严重副作用。更重要的是,总有替代方案可避免使用终结方法

关键点需注意:finalize从Java 9开始已被弃用——并最终将被移除。

本教程源代码可在GitHub获取。


原始标题:A Guide to the finalize Method in Java