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"); // 清理逻辑
}
}
}
关键步骤:
- 获取
Cleaner
实例:Cleaner.create()
- 注册清理动作:
cleaner.register(object, action)
- 执行清理:
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 弃用终结机制是必然选择,其缺陷(不可预测性、性能开销、线程不安全)已不适应现代开发需求。推荐替代方案:
- **优先使用
try-with-resources
**:简洁可靠,适用于短生命周期资源 - Cleaner API:适用于需要延迟清理的长生命周期资源
源代码可在 GitHub 获取。告别终结机制,拥抱更健壮的资源管理方式!