finalize()为什么被废弃JDK9:对象销毁的正确姿势是什么
finalize()为什么被废弃JDK9:对象销毁的正确姿势是什么
适读人群:Java中高级开发者、关注JVM内存管理的后端工程师 | 阅读时长:约13分钟 | 文章类型:演进历史+原理分析+替代方案
开篇故事
很久以前,我在一个老项目里看到这样的代码:
public class ConnectionWrapper {
private Connection conn;
public ConnectionWrapper(String url) {
this.conn = DriverManager.getConnection(url);
}
@Override
protected void finalize() throws Throwable {
if (conn != null && !conn.isClosed()) {
conn.close(); // 在对象被GC时关闭连接
}
super.finalize();
}
}我问写这代码的老张(和我同名,为了区分叫他老张二号):为什么要用finalize关闭连接?
他说:finalize是析构函数,对象要销毁时自动调用,保证资源释放。
我说:这个想法是对的,但finalize有严重的问题,你这么写反而会导致连接泄漏。
他不信,觉得finalize就是Java的析构函数。
然后我花了半个小时给他讲了finalize的坑,他才恍然大悟,立刻重写了这段代码。
一、finalize()的设计初衷和现实问题
设计初衷
finalize()是Object的一个protected方法,设计初衷是:当垃圾回收器判定对象不再可达时,在真正回收内存之前调用这个方法,让对象有机会做最后的清理(释放本地资源、关闭文件等)。
听起来很美好,但实际上,它几乎在每一个方面都是有问题的。
问题1:执行时机不确定
Java规范只保证:如果一个对象有finalize方法,GC在回收它之前某个时间会调用。但"某个时间"可以是几秒后,可以是几分钟后,甚至在JVM退出前都不调用。
如果你靠finalize来释放数据库连接,连接可能在几分钟内都不会被释放,连接池耗尽,系统崩溃。
问题2:finalize可能根本不执行
JVM并不保证finalize一定会执行。如果JVM突然退出(System.exit(),进程kill),finalize可能完全不调用。
问题3:finalize可以让对象"复活"
这是finalize最奇葩的特性:在finalize里,可以把this赋给某个还可达的引用,让这个对象"复活",避免被GC回收。
static Object instance;
@Override
protected void finalize() {
instance = this; // 把自己赋给静态变量,对象"复活"
}这让GC的行为变得不可预测,也是Java GC比C++析构函数复杂得多的原因之一。
问题4:严重影响GC性能
有finalizer的对象,GC需要特殊处理:把它们加入一个专门的Finalizer队列,由一个低优先级的Finalizer线程来执行它们的finalize方法。这个线程可能跟不上对象被创建的速度,导致大量等待finalize的对象积压,内存泄漏。
这就是为什么JDK 9把finalize标记为@Deprecated(forRemoval=true),JDK 18废弃了finalization,未来版本会彻底移除。
二、核心原理深挖
finalize的处理流程(JVM视角)
有finalizer的对象,至少需要两次GC才能被回收:第一次GC把它放到Finalizer队列,等Finalizer线程执行完finalize后,第二次GC才真正回收内存。
三、完整代码实现
代码一:验证finalize的不确定性
package com.laozhang.finalizer;
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
/**
* finalize行为验证与替代方案演示
*/
public class FinalizeDemo {
// ===== 1. 展示finalize的不确定性 =====
static class ResourceWithFinalizer {
private final String name;
ResourceWithFinalizer(String name) {
this.name = name;
System.out.println("[" + name + "] 资源已创建");
}
@Override
@Deprecated // JDK 9+ 已废弃
protected void finalize() throws Throwable {
System.out.println("[" + name + "] finalize被调用(时机不可控!)");
super.finalize();
}
}
// ===== 2. AutoCloseable(推荐替代方案)=====
static class SafeResource implements AutoCloseable {
private final String name;
private boolean closed = false;
SafeResource(String name) {
this.name = name;
System.out.println("[" + name + "] 资源已创建");
}
public void doWork() {
if (closed) throw new IllegalStateException("资源已关闭: " + name);
System.out.println("[" + name + "] 执行工作");
}
@Override
public void close() {
if (!closed) {
closed = true;
System.out.println("[" + name + "] 资源已关闭(确定性释放)");
}
}
}
// ===== 3. PhantomReference:finalize的现代替代品 =====
static class ManagedResource {
private final String name;
ManagedResource(String name) { this.name = name; }
public String getName() { return name; }
}
/**
* 使用PhantomReference监听对象GC,做清理工作
* 这是finalize的正确替代方案
*/
static class ResourceCleaner {
// 引用队列:当对象被GC时,其PhantomReference会被放入队列
private final ReferenceQueue<ManagedResource> queue = new ReferenceQueue<>();
private final Thread cleanerThread;
ResourceCleaner() {
cleanerThread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
// 阻塞等待,直到有对象被GC
PhantomReference<?> ref = (PhantomReference<?>) queue.remove();
System.out.println("清理线程:有对象被GC,执行清理工作");
// 这里执行实际的清理逻辑(关闭连接、释放本地资源等)
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "resource-cleaner");
cleanerThread.setDaemon(true);
cleanerThread.start();
}
PhantomReference<ManagedResource> register(ManagedResource resource) {
// 注意:不要在PhantomReference里持有resource的强引用,否则永远不会被GC
return new PhantomReference<>(resource, queue);
}
}
// ===== 4. Java 9+ Cleaner(官方推荐替代方案)=====
static class ModernResource implements AutoCloseable {
// java.lang.ref.Cleaner(JDK 9+)
private static final java.lang.ref.Cleaner CLEANER = java.lang.ref.Cleaner.create();
private final String name;
private final java.lang.ref.Cleaner.Cleanable cleanable;
// 注意:这个Runnable不能持有ModernResource的引用(会导致循环引用,永远不GC)
// 所以用静态内部类或者只捕获必要的数据
private static class CleanAction implements Runnable {
private final String resourceName;
CleanAction(String name) { this.resourceName = name; }
@Override
public void run() {
System.out.println("[" + resourceName + "] Cleaner执行清理(对象即将被GC)");
// 在这里执行实际的清理工作:关闭本地资源等
}
}
ModernResource(String name) {
this.name = name;
// 注册清理动作:当ModernResource对象被GC时,Cleaner会执行CleanAction
this.cleanable = CLEANER.register(this, new CleanAction(name));
System.out.println("[" + name + "] 资源已创建");
}
@Override
public void close() {
// 主动关闭:立即执行清理,不等GC
cleanable.clean();
System.out.println("[" + name + "] 资源主动关闭");
}
}
public static void main(String[] args) throws Exception {
System.out.println("=== 1. AutoCloseable(推荐)===");
try (SafeResource res = new SafeResource("DB-Connection")) {
res.doWork();
} // 自动调用close(),确定性释放
System.out.println("\n=== 2. Cleaner(JDK 9+,作为安全网)===");
try (ModernResource res = new ModernResource("NativeBuffer")) {
System.out.println("使用资源中...");
} // 主动close
// 即使忘了close,Cleaner也会在GC时做清理
ModernResource forgotten = new ModernResource("ForgottenResource");
forgotten = null; // 失去引用
System.gc(); // 触发GC(生产中不要手动触发GC)
Thread.sleep(100); // 给GC和Cleaner线程时间
System.out.println("\n=== 3. 不使用finalize的原则 ===");
System.out.println("原则:优先实现AutoCloseable + try-with-resources");
System.out.println("安全网:可选用Cleaner(非finalize)");
System.out.println("禁止:不要依赖finalize进行资源释放");
}
}代码二:实际资源管理的正确模式
package com.laozhang.finalizer;
import java.lang.ref.Cleaner;
import java.nio.ByteBuffer;
/**
* 真实项目中资源管理的最佳实践
* 模拟一个管理本地内存(DirectByteBuffer风格)的类
*/
public class DirectMemoryManager implements AutoCloseable {
private static final Cleaner CLEANER = Cleaner.create();
// 模拟本地内存地址
private long nativeAddress;
private final int capacity;
private volatile boolean closed = false;
// 清理动作:不能持有DirectMemoryManager的强引用
private static class CleanAction implements Runnable {
private final long address;
private final int capacity;
CleanAction(long address, int capacity) {
this.address = address;
this.capacity = capacity;
}
@Override
public void run() {
if (address != 0) {
System.out.println("Cleaner:释放本地内存,地址=" + address + ", 大小=" + capacity);
// unsafe.freeMemory(address); // 实际的本地内存释放
}
}
}
private final Cleaner.Cleanable cleanable;
public DirectMemoryManager(int capacity) {
this.capacity = capacity;
this.nativeAddress = allocateNativeMemory(capacity);
// 注册清理动作:当DirectMemoryManager实例被GC时,执行CleanAction
this.cleanable = CLEANER.register(this, new CleanAction(nativeAddress, capacity));
System.out.println("分配本地内存:地址=" + nativeAddress + ", 大小=" + capacity);
}
private static long allocateNativeMemory(int capacity) {
// 模拟分配本地内存(实际项目用Unsafe.allocateMemory)
return (long) (Math.random() * 1_000_000) + 1;
}
public void write(int index, byte value) {
checkOpen();
if (index < 0 || index >= capacity) throw new IndexOutOfBoundsException();
// 实际写入本地内存
System.out.println("写入本地内存[" + index + "] = " + value);
}
private void checkOpen() {
if (closed) throw new IllegalStateException("内存已释放");
}
@Override
public void close() {
if (!closed) {
closed = true;
cleanable.clean(); // 主动触发清理(比等GC更及时)
}
}
/**
* 演示正确的使用模式
*/
public static void main(String[] args) throws Exception {
System.out.println("=== 使用try-with-resources(推荐)===");
try (DirectMemoryManager mem = new DirectMemoryManager(1024)) {
mem.write(0, (byte) 'H');
mem.write(1, (byte) 'i');
} // close自动调用,立即释放内存
System.out.println("\n=== 忘记close时,Cleaner作为安全网 ===");
DirectMemoryManager leakyMem = new DirectMemoryManager(512);
leakyMem.write(0, (byte) 42);
// 忘记调用close
leakyMem = null; // 对象失去引用
System.gc();
Thread.sleep(200); // Cleaner线程有机会运行
// Cleaner会在GC时释放内存,但不如主动close及时
System.out.println("\n=== 关键原则 ===");
System.out.println("1. 主动路径:try-with-resources,100%及时释放");
System.out.println("2. 安全网:Cleaner处理忘记close的情况");
System.out.println("3. 禁止:不用finalize");
}
}四、踩坑实录
坑1:靠finalize关闭数据库连接,连接池耗尽
报错现象:
com.mysql.cj.jdbc.exceptions.CommunicationsException:
Communications link failure...
Caused by: java.sql.SQLTransientConnectionException:
HikariPool-1 - Connection is not available, request timed out after 30000ms.连接池连接耗尽,新的数据库请求超时。
根本原因:
用finalize关闭连接,但finalize执行时机不确定,大量连接长时间未被归还连接池。
具体解法:
// 使用try-with-resources,连接用完立刻归还
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
// 执行查询
} // conn和ps自动关闭/归还坑2:Finalizer线程积压,内存OOM
报错现象:
java.lang.OutOfMemoryError: Java heap space
at ...Heap dump里发现大量sun.misc.Finalizer对象和等待finalize的业务对象。
根本原因:
某个类重写了finalize,但该类的对象创建速度远超过Finalizer线程的处理速度。Finalizer队列积压,大量对象无法被GC,内存撑满。
具体解法:
删除或改造finalize方法,用AutoCloseable + Cleaner代替。短期修复可以把Finalizer线程优先级调高(但治标不治本)。
坑3:finalize中的对象复活导致GC行为异常
报错现象:
系统内存持续增长,GC日志里有大量对象在多次GC后仍然存活(finalize后复活的对象)。
根本原因:
private static List<SomeClass> survivors = new ArrayList<>();
@Override
protected void finalize() {
survivors.add(this); // 每次即将被GC就把自己放进静态List,永远不死
}这是一个显著的内存泄漏,finalize每次都让对象"复活",最终survivors积累了大量对象。
具体解法:
不要在finalize里让对象复活。删除这段代码,分析为什么这些对象需要存活那么久,从根本上修复设计。
五、总结与延伸
finalize的废弃,在Java的历史里是一个里程碑。它代表了Java社区在近30年的实践中,终于正式承认了一个设计错误,并提供了更好的替代方案。
替代方案的选择:
| 场景 | 推荐方案 |
|---|---|
| 管理文件/网络/DB等资源 | AutoCloseable + try-with-resources |
| 管理本地内存/JNI资源 | Cleaner(JDK 9+)作为安全网 |
| 需要在对象GC后执行某些清理 | PhantomReference + ReferenceQueue |
| 以上所有场景 | 绝对不用finalize |
如果你现在的代码里还有finalize,是时候重构了。现代Java(尤其是JDK 9+)的Cleaner API设计得很优雅,兼具"用完即关"的确定性和"万一忘了关"的安全网,是管理非内存资源的正确工具。
