1. Introduction
1.导言
In this tutorial, we’ll discuss Java’s attempt to deprecate Object finalization for removal with JEP 421 of the Java 18 release. We’ll also talk about potential replacements and better alternatives for finalization.
在本教程中,我们将讨论 Java 在 JEP 421 的 Java 18 版本中试图废除 Object 最终确定的做法。我们还将讨论最终确定的潜在替代方法和更好的替代方案。
2. Finalization in Java
2.Java 中的最终完成
2.1. Resource Leaks
2.1.资源泄漏
The JVM comes with Garbage Collection (GC) mechanisms to reclaim memory of objects that are no longer in use by the application, or no more references that are pointing to the object. However, some object references use and represent other underlying resources, such as OS-level resources, native memory blocks, and open file descriptors. These objects should call the close() method while shutting down to release the underlying resource back to the OS.
JVM自带垃圾回收(GC)机制,用于回收应用程序不再使用的对象的内存,或不再有指向对象的引用。 但是,某些对象引用使用并代表其他底层资源,例如操作系统级资源、本地内存块和打开的文件描述符。这些对象在关闭时应调用 close() 方法,以便将底层资源释放回操作系统。
If the GC cleans up the objects prematurely before the object has had a chance to call the close(), the OS considers the object to be in use. This is a resource leak.
如果 GC 在对象有机会调用 close() 之前过早地清理了对象,操作系统就会认为该对象仍在使用中。这就是资源泄漏。
A very common example of this is when we are trying to read a file and wrap our code in a try-catch block to handle exceptions. We wrap the graceful closure of resources in the traditional finally block. This is not a completely foolproof solution, as exceptions can happen even in the finally block, leading to resource leaks:
一个很常见的例子是,当我们试图读取文件时,我们会将代码封装在 try-catch 块中以处理异常。我们在传统的 finally 块中封装了资源的优雅关闭。这并不是一个完全万无一失的解决方案,因为即使在 finally 块中也可能发生异常,从而导致资源泄漏:
public void copyFileOperation() throws IOException {
try {
fis = new FileInputStream("input.txt");
// perform operation on the file
fis.close();
} finally {
if (fis != null) {
fis.close();
}
}
}
2.2. Object’s finalize() Method
2.2.对象的 finalize() 方法
Java introduced the idea of finalization to deal with resource leaks. The finalize() method, also called the finalizer, is a protected void method in the Object class whose purpose is to release any resource the object uses. We override the method in our class to perform the closure of resources to help the GC:
Java 引入了最终化的思想来处理资源泄漏。finalize()方法(也称为最终器)是 Object 类中一个受保护的 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();
}
}
When the object is eligible for garbage collection, the garbage collector calls the finalize() method of the object if it is overridden. Although having a finalize() method in a class to perform all resource cleanup work looks like a good way to handle resource leaks, it has been stated for deprecation itself since Java 9. Finalization in itself has a couple of fundamental flaws.
当对象符合垃圾回收条件时,如果对象的finalize()方法被重载,垃圾回收器将调用该方法。尽管在类中使用 finalize() 方法来执行所有资源清理工作看起来是一种处理资源泄漏的好方法,但自 Java 9 以来,该方法本身已被声明将被淘汰。finalization 本身存在一些基本缺陷。
3. Flaws of Finalization
3.最终确定的缺陷
3.1. Unpredictable Execution
3.1.不可预测的执行
There is no guarantee that the object’s finalize() will be called even when the object is eligible for garbage collection. Similarly, there can be an unpredictable latency for the GC to call the object’s finalizer after the object is eligible for garbage collection.
即使在对象符合垃圾回收条件时,也无法保证对象的 finalize() 会被调用。同样,在对象符合垃圾回收条件后,GC 调用对象的终结器可能会出现不可预测的延迟。
The finalizer is scheduled to be run by the GC, however, garbage collection happens based on parameters including the system’s current memory needs. If GC is paused because of ample free memory, many objects will wait on the heap for their finalizer to be called. This may lead to resource shortages.
终结器计划由 GC 运行,但垃圾回收是根据系统当前内存需求等参数进行的。如果由于可用内存充足而暂停 GC,许多对象将在堆上等待调用其最终器。这可能会导致资源短缺。
3.2. Unconstrained Finalizer Code
3.2.无约束终结器代码
Even though the intention of the finalize() method is defined, the code is still something that a developer puts and it can take any action. This lack of control can defeat the purpose of the finalizer. This also introduces a security threat to the application. Malicious code can sit in the finalizer and cause unexpected errors or lead to the application misbehaving in various ways.
即使定义了 finalize() 方法的意图,但代码仍然是开发人员编写的,它可以执行任何操作。这种缺乏控制的情况可能会破坏 finalizer 的目的。这还会给应用程序带来安全威胁。恶意代码可能存在于 finalizer 中,并导致意外错误或以各种方式导致应用程序行为失常。
If we omit the finalizer altogether, a subclass can still define a finalize() for itself and gain access to ill-formed or deserialized objects. The subclass may also choose to override the parent’s finalizer and inject malicious code.
如果我们完全省略终结器,子类仍然可以为自己定义一个 finalize() 并访问畸形或反序列化对象。子类还可以选择覆盖父类的终结器,并注入恶意代码。
3.3. Performance Overhead
3.3.性能开销
The presence of an overridden finalize() in classes adds a performance penalty, as the GC needs to track all such classes with finalizers. The GC also needs to perform additional steps in such an object’s lifecycle, especially during object creation and finalization.
类中存在重载的 finalize() 会增加性能损失,因为 GC 需要跟踪所有带有最终器的此类类。
There are some garbage collectors that are throughput-oriented and perform best by minimizing overall pause times of garbage collection. For such garbage collectors, the finalizer leads to a disadvantage as it increases pause times.
有些垃圾收集器以吞吐量为导向,通过最大限度地减少垃圾收集的整体暂停时间来实现最佳性能。对于这类垃圾收集器来说,终结器会增加 暂停时间,从而导致劣势。
Additionally, the finalize() method is always enabled, and GC will call the finalizer even if it is not required. The finalize() action cannot be canceled even if the requirement of closing the resources is already handled.
此外,finalize() 方法始终处于启用状态,即使不需要,GC 也会调用最终器。 即使已经处理了关闭资源的要求,也不能取消 finalize() 操作。
This leads to a performance penalty, as it is always called regardless of its requirement.
这会导致性能下降,因为无论是否需要,它都会被调用。
3.4. No Thread Guarantee
3.4.无螺纹保证
The JVM does not guarantee which thread will invoke the object’s finalizer, nor does it guarantee the order. There can be an unspecified number of finalizer threads. In case the application threads allocate resources to objects more frequently than finalizer threads can relinquish the resources, it can lead to resource shortages as well.
JVM 并不保证哪个线程将调用对象的终结器,也不保证调用顺序。如果应用程序线程为对象分配资源的频率高于最终器线程放弃资源的频率,那么也会导致资源短缺。
3.5. Ensuring the Correctness of the Finalizer Code
3.5.确保终结器代码的正确性
It is generally difficult to write a correct finalize() implementation. It is also very easy to write code that breaks the application because of an improperly implemented finalizer. An object’s finalize() method must remember to invoke the finalize() of its parent class with super.finalize(), and it is not supplied by the JVM inherently.
通常很难编写正确的 finalize() 实现。同时,也很容易编写出因最终器实现不当而导致应用程序崩溃的代码。对象的 finalize() 方法必须记住通过 super.finalize() 调用其父类的 finalize(),而且 JVM 本身并不提供该方法。
As finalizers are run on an unspecified number of threads, it can lead to issues that are common to a multithreaded environment, such as deadlocks and any other threading problem. Moreover, when there are several classes with finalizers, it leads to increased coupling in the system. There can arise interdependencies in the finalization of these objects, and some objects might stay in the heap more, waiting for a dependent object to be finalized.
由于终结器在未指定数量的线程上运行,它可能会导致多线程环境中常见的问题,如死锁和其他任何线程问题。此外,当多个类都有最终执行器时,系统中的耦合性也会增加。在这些对象的最终化过程中,可能会出现相互依赖的情况,一些对象可能会在堆中停留更长时间,等待依赖对象的最终化。
4. try-with-resources as an Alternative Technique
4.尝试使用资源作为替代技术
One of the ways we can guarantee that a resource’s close() method is called is by using the try-with resource construct that Java 7 introduced. This framework is an improvement over the try-catch-finally construct as it makes sure all exceptions are handled properly, thereby removing the requirement of finalization:
我们可以通过使用 Java 7 引入的 try-with resource 结构来保证调用资源的 close() 方法。该框架是对 try-catch-finally 结构的改进,因为它能确保所有异常都得到正确处理,从而消除了最终确定的要求:
public void readFileOperationWithTryWith() throws IOException {
try (FileOutputStream fis = new FileOutputStream("input.txt")) {
// perform operations
}
}
We can put any number of resource initializations inside the try-with block. Let’s rewrite our class without a finalizer:
我们可以在 try-with 代码块中加入任意数量的资源初始化。让我们重写没有 finalizer 的类:
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();
}
}
The only difference here is the AutoCloseable interface and the overridden close() method. Now we can safely use our resource object inside a try-with block and not worry about resource leak:
这里唯一的区别是 AutoCloseable 接口和重载的 close() 方法。现在,我们可以在 try-with 代码块中安全地使用资源对象,而不必担心资源泄漏:
@Test
public void givenCloseableResource_whenUsingTryWith_thenShouldClose() throws IOException{
int length = 0;
try (MyCloseableResourceClass mcr = new MyCloseableResourceClass()) {
length = mcr.getByteLength();
}
Assert.assertEquals(20, length);
}
5. Cleaner API in Java
5.Java 中的清洁应用程序接口
5.1. Creating a Resource Class With the Cleaner API
5.1.使用清理 API 创建资源类
Java 9 introduced the idea of a Cleaner API for releasing long-lived resources. Cleaners implement the Cleanable interface and allow us to define and register cleanup actions against objects.
Java 9 引入了用于释放长期资源的 Cleaner API。Cleaners 实现了 Cleanable 接口,并允许我们定义和注册针对对象的清理操作。
There are three steps to implementing a cleaner for our resource class:
为我们的资源类实施清洁器有三个步骤:
- fetching a Cleaner instance
- registering a cleaning action
- perform the cleaning
We define our resource class, which will use the Cleaner API to help us clean the resource after use:
我们定义了资源类,它将使用 Cleaner API 帮助我们在使用后清理资源:
public class MyCleanerResourceClass implements AutoCloseable {
private static Resource resource;
}
To obtain a cleaner instance, we call the static create() method on the Cleaner class:
要获得清洁器实例,我们需要调用 Cleaner 类的静态 create() 方法:
private static final Cleaner cleaner = Cleaner.create();
private final Cleaner.Cleanable cleanable;
We also create a Cleanable instance, which will help us register the cleaning actions against my object:
我们还创建了一个 Cleanable 实例,它将帮助我们针对我的对象注册清洁操作:
public MyCleanerResourceClass() {
resource = new Resource();
this.cleanable = cleaner.register(this, new CleaningState());
}
The register() method of a cleaner takes two arguments, the object it is supposed to monitor for cleaning, and the action to perform for cleaning. We pass a lambda of type java.lang.Runnable here for the cleaning action, which is defined in the CleaningState class:
清洁器的 register() 方法需要两个参数,一个是要监控以进行清洁的对象,另一个是要执行的清洁操作。我们在这里为清洁操作传递了一个 java.lang.Runnable 类型的 lambda,该操作在 CleaningState 类中定义:
static class CleaningState implements Runnable {
CleaningState() {
// constructor
}
@Override
public void run() {
// some cleanup action
System.out.println("Cleanup done");
}
}
We also override the close() method as we have implemented the AutoCloseable interface. In the close() method, we invoke the clean() method on the cleanable and perform the third and final step.
我们还重载了 close() 方法,因为我们已经实现了 AutoCloseable 接口。在 close() 方法中,我们调用了 cleanable 上的 clean() 方法,并执行了第三步,也是最后一步。
@Override
public void close() {
// perform actions to close all underlying resources
this.cleanable.clean();
}
5.2. Testing the Cleaner Implementation
5.2.测试清洁器的实施
Now that we have implemented a cleaner API for our resource class, let’s validate it by writing a small test:
现在,我们已经为资源类实现了更简洁的 API,让我们通过编写一个小测试来验证它:
@Test
public void givenMyCleanerResource_whenUsingCleanerAPI_thenShouldClean() {
assertDoesNotThrow(() -> {
try (MyCleanerResourceClass myCleanerResourceClass = new MyCleanerResourceClass()) {
myCleanerResourceClass.useResource();
}
});
}
Notice that we are wrapping our resource class inside a try-with block. On running the test, we can see in the console, the two statements:
请注意,我们将资源类封装在一个 try-with 块中。运行测试时,我们可以在控制台中看到以下两条语句:
Using the resource
Cleanup done
5.3. Advantages of the Cleaner API
5.3.清理 API 的优势
When the object is eligible for cleanup, the cleaner API performs automatic cleaning up of the resources. The cleaner API attempts to solve most of the drawbacks of finalization that are mentioned above. In finalize(), we could write code that would resurrect the object and make it unworthy for collection. This issue is not there in the cleaner API, as the CleaningState object cannot access the original object.
当对象符合清理条件时,清理程序接口会自动执行资源清理。清洁 API 试图解决上述最终确定的大部分缺点。在 finalize() 中, 我们可以编写代码来复活对象,使其不值得被收集。在清洁 API 中不存在这个问题,因为 CleaningState 对象无法访问原始对象。
Additionally, the cleaner API requires proper registration of the cleaning action on the object, which is done after the object creation is complete. Therefore, the cleaning action can’t process improperly initialized objects. Moreover, this sort of cleaning action is cancellable, unlike finalization.
此外,清理 API 还要求在对象上正确注册清理操作,这是在对象创建完成后进行的。因此,清理操作无法处理初始化不当的对象。此外,这种清理操作是可以取消的,这一点与最终处理不同。
Finally, cleaning actions run on separate threads and are hence non-interfering, and exceptions thrown by the cleaning action are auto-ignored by the JVM.
最后,清理操作在单独的线程上运行,因此不会造成干扰,而且清理操作抛出的异常会被 JVM 自动忽略。
6. Conclusion
6.结论
In this article, we talked about the reason behind Java’s decision to deprecate finalization for removal. We looked at the problems with finalization and explored two alternative solutions that help in resource cleanup.
在这篇文章中,我们讨论了 Java 决定弃用最终确定删除功能背后的原因。我们研究了最终确定的问题,并探讨了两种有助于资源清理的替代解决方案。
As always, the source code for this tutorial can be found over on GitHub.