杰瑞科技汇

Java调用finalize方法需要注意什么?

核心结论先行

在 Java 9 及更高版本中,finalize() 方法已被废弃(deprecated),从 Java 12 开始,它被移除,变为一个 private 方法。

Java调用finalize方法需要注意什么?-图1
(图片来源网络,侵删)

在现代 Java 开发中,你不应该、也不需要手动调用 finalize(),它的设计初衷是为了在对象被垃圾回收器回收前,执行一些资源清理操作,但这种机制并不可靠,且已被更优越的替代方案所取代。


finalize() 的作用和问题

finalize() 的作用

finalize()Object 类的一个方法,任何 Java 类都继承了这个方法,它的设计意图是:

当垃圾回收器确定不存在对该对象的任何更多引用时,由对象的垃圾回收器调用此方法,子类覆盖 finalize 方法以配置系统资源或执行其他清理。

它就像一个“临终仪式”,在对象即将被“处决”(GC回收)前,给它一个机会进行自我清理。

Java调用finalize方法需要注意什么?-图2
(图片来源网络,侵删)

finalize() 的严重问题

尽管初衷良好,但 finalize 机制存在许多致命缺陷,导致它极不可靠:

  1. 执行时机不确定

    • finalize() 的调用完全由垃圾回收器(GC)决定,你无法预测它何时会被执行,可能是在对象不再被引用后立即执行,也可能是在程序结束前,甚至永远不会执行(程序因内存不足而崩溃,或者内存足够大,GC 根本没有被触发)。
    • 这使得 finalize() 不适合用于需要及时释放的资源,如数据库连接、文件句柄、网络套接字等。
  2. 性能开销大

    • 使用 finalize 的对象,其回收过程会变得非常复杂和缓慢,GC 需要经历两个阶段:标记阶段终结阶段
    • 在终结阶段,GC 会将对象标记为“待终结”,然后在一个新的线程中执行其 finalize() 方法,这个过程会显著拖慢垃圾回收的效率,增加 GC 的停顿时间,影响整个应用的性能。
  3. 执行顺序不可控

    Java调用finalize方法需要注意什么?-图3
    (图片来源网络,侵删)
    • 如果对象 A 持有对象 B 的引用,且两个类都重写了 finalize(),GC 会以不确定的顺序调用它们的 finalize() 方法,这可能导致资源依赖关系错乱,引发难以排查的问题。
  4. 可能导致对象复活

    • finalize() 方法内部,如果将对象的 this 引赋值给一个静态变量或其他可达引用,那么这个对象就被“复活”了,垃圾回收器会放弃回收它,这会使得 finalize() 被再次调用,形成死循环,逻辑非常混乱。

如何“手动”调用 finalize()

虽然不推荐,但从技术层面看,你有两种方式可以“强制”执行 finalize() 的逻辑。

直接调用(强烈不推荐)

你可以像调用普通方法一样,直接在一个对象实例上调用 finalize()

public class MyResource {
    private String name;
    public MyResource(String name) {
        this.name = name;
        System.out.println("资源 " + name + " 被创建");
    }
    @Override
    protected void finalize() throws Throwable {
        // 这是清理逻辑
        System.out.println("资源 " + name + " 的 finalize() 被调用,正在清理...");
        // ... 清理代码 ...
    }
}
public class Main {
    public static void main(String[] args) {
        MyResource resource = new MyResource("手动调用");
        // 直接调用 finalize()
        // 注意:即使调用了,对象也依然存在,不会被回收
        try {
            resource.finalize();
        } catch (Throwable t) {
            t.printStackTrace();
        }
        System.out.println("手动调用 finalize() 后,对象依然存在: " + resource);
    }
}

输出结果:

资源 手动调用 被创建
资源 手动调用 的 finalize() 被调用,正在清理...
手动调用 finalize() 后,对象依然存在: com.example.MyResource@15db9742

为什么这么做是错误的?

  • 混淆了“清理”和“回收”finalize() 是为 GC 回收服务的,你手动调用它,只是执行了一段清理代码,但对象本身仍然存活,占用内存,这完全违背了 GC 的自动管理原则。
  • 可能被重复执行:如果这个对象后续真的被 GC 回收了,它的 finalize() 会被 GC 再次调用,导致清理逻辑被执行两次,可能引发错误。

通过 System.runFinalization()(不推荐)

这个方法会建议 JVM 执行所有待终结对象的 finalize() 方法。

public class Main {
    public static void main(String[] args) {
        MyResource resource = new MyResource("通过runFinalization");
        resource = null; // 断开引用,使其成为 GC 候选人
        // 建议JVM执行所有待终结对象的finalize方法
        System.runFinalization(); 
        // 注意:这只是一个“建议”,JVM可以忽略它
        // 并且它不会立即执行GC
    }
}

为什么这么做是错误的?

  • 仍然是建议,而非保证runFinalization() 只是向 JVM 提交了一个请求,它不保证会立即执行,甚至不保证会执行。
  • 性能问题:同样会触发复杂的终结流程,影响性能。

正确的资源管理方式(finalize 的替代方案)

既然 finalize 不可靠,我们应该使用哪些现代、可靠的方式来管理资源呢?

try-finally 块(最经典、最可靠)

这是最基础、最通用的方式,适用于任何需要释放资源的场景。

import java.io.FileInputStream;
import java.io.IOException;
public class TryFinallyExample {
    public static void main(String[] args) {
        FileInputStream fis = null;
        try {
            fis = new FileInputStream("myfile.txt");
            // 使用 fis 读取文件...
            int data = fis.read();
            System.out.println("读取到字节: " + data);
        } catch (IOException e) {
            System.err.println("文件读取出错: " + e.getMessage());
        } finally {
            // 确保资源一定会被关闭
            if (fis != null) {
                try {
                    fis.close();
                    System.out.println("文件流已关闭");
                } catch (IOException e) {
                    System.err.println("关闭文件流时出错: " + e.getMessage());
                }
            }
        }
    }
}

优点

  • 100% 可靠:无论 try 块中是否发生异常,finally 块中的代码一定会被执行
  • 简单直观:所有 Java 开发者都熟悉。

try-with-resources(Java 7+,推荐)

这是 Java 7 引入的语法糖,是 try-finally 的现代化、更简洁的替代方案,它要求资源实现了 AutoCloseable 接口(Closeable 是其子接口)。

import java.io.FileInputStream;
import java.io.IOException;
public class TryWithResourcesExample {
    public static void main(String[] args) {
        // try-with-resources 语句
        try (FileInputStream fis = new FileInputStream("myfile.txt")) {
            // 使用 fis 读取文件...
            int data = fis.read();
            System.out.println("读取到字节: " + data);
        } catch (IOException e) {
            System.err.println("文件读取出错: " + e.getMessage());
        }
        // 不需要手动调用 fis.close()!
        // try 块执行完毕后,JVM会自动调用 fis.close()
        System.out.println("文件流已自动关闭");
    }
}

优点

  • 代码简洁:消除了冗余的 finally 块和 null 检查。
  • 更安全:即使在 try 块中发生异常,资源也会被正确关闭。
  • 可以管理多个资源:用分号隔开即可。
    try (FileInputStream fis = new FileInputStream("in.txt");
         FileOutputStream fos = new FileOutputStream("out.txt")) {
        // ...
    }

Cleaner API(Java 9+,finalize 的现代替代品)

Cleanerjava.lang.ref 包下的一个类,它提供了一个更可靠、更可控的机制来执行清理操作,是 finalize 的官方替代品。

它的工作原理是:

  1. 创建一个 Cleaner 实例。
  2. 为你的对象创建一个“清理操作”(一个实现了 Runnable 接口的类)。
  3. 调用 cleaner.register(obj, runnable),将对象和清理操作关联起来。

当这个对象变成垃圾时,GC 会发现它关联了 Cleaner,并异步地执行注册的 Runnable 任务。

import java.lang.ref.Cleaner;
// 需要被清理的资源类
class MyResourceWithCleaner {
    private final String name;
    // 使用静态的Cleaner实例,通常由一个公共的Service提供
    private static final Cleaner cleaner = Cleaner.create();
    MyResourceWithCleaner(String name) {
        this.name = name;
        System.out.println("资源 " + name + " 被创建");
        // 注册清理操作
        cleaner.register(this, new MyResourceCleaner(name));
    }
    // 清理逻辑被封装在一个独立的Runnable中
    private static class MyResourceCleaner implements Runnable {
        private final String resourceName;
        MyResourceCleaner(String name) {
            this.resourceName = name;
        }
        @Override
        public void run() {
            // 这是真正的清理逻辑
            System.out.println("资源 " + resourceName + " 被Cleaner异步清理");
        }
    }
}
public class CleanerExample {
    public static void main(String[] args) throws InterruptedException {
        MyResourceWithCleaner resource = new MyResourceWithCleaner("Cleaner示例");
        resource = null; // 断开引用,使其成为GC的候选人
        // 提示GC运行,以便看到清理效果
        // 在真实应用中,你不需要这样做
        System.gc();
        // 给 Cleaner 一些时间来执行异步任务
        Thread.sleep(500); 
        System.out.println("主线程结束");
    }
}

输出结果:

资源 Cleaner示例 被创建
主线程结束
资源 Cleaner示例 被Cleaner异步清理

优点

  • 更可靠:比 finalize 更接近 GC 的行为,但避免了 finalize 的许多问题。
  • 性能更好:终结过程比 finalize 更高效。
  • 不会复活对象Cleaner 的任务执行是单向的,不会让对象复活。

缺点

  • 仍然是异步的:执行时机依然不确定。
  • 不适合需要立即释放的资源:与 finalize 一样,不适合数据库连接等需要立即归还的资源。

特性 finalize() (已废弃) try-finally try-with-resources (推荐) Cleaner API
可靠性 极低,时机不确定 极高,一定会执行 极高,一定会执行 较高,但时机不确定
执行时机 GC 决定,可能永不执行 立即执行 立即执行 GC 决定,异步执行
性能 差,拖慢 GC 较好
代码简洁性 N/A 冗长 简洁 较为复杂
适用场景 不推荐在任何场景使用 所有需要手动释放资源的场景 所有实现了 AutoCloseable 的资源 本地资源(如文件句柄、内存块)的清理
Java 版本 1 - 8 (已移除) 所有版本 Java 7+ Java 9+

最终建议:

  1. 对于文件、网络流、数据库连接等始终使用 try-with-resources,这是最现代、最安全、最简洁的方式。
  2. 对于任何需要手动释放的资源:使用 try-finally 块作为后备方案。
  3. 绝对不要:依赖 finalize() 或手动调用它。
  4. 仅在特定高级场景(如管理 JNI 调用分配的本地内存)下,可以考虑使用 Cleaner API,并充分理解其异步执行的特性。
分享:
扫描分享到社交APP
上一篇
下一篇