在本教程中,我们将重点介绍 Java 语言的核心方面 - 根 Object 类提供的 finalize 方法。
简而言之,这是在特定对象的垃圾回收之前调用的。
finalize() 方法称为终结器。
当 JVM 确定此特定实例应该被垃圾回收时,将调用终结器。此类终结器可以执行任何操作,包括使对象恢复生命状态。
但是,终结器的主要目的是在对象从内存中删除之前释放对象使用的资源。终结器可以用作清理操作的主要机制,也可以在其他方法失败时用作安全网。
为了理解终结器的工作原理,让我们看一下类声明:
类 Finalizable 有一个字段读取器,该读取器引用可关闭的资源。从此类创建对象时,它会构造一个新的 BufferedReader 实例,该实例从类路径中的文件读取。
在 readFirstLine 方法中使用这样的实例来提取给定文件中的第一行。请注意,读取器在给定的代码中未关闭。
我们可以使用终结器来做到这一点:
很容易看出,终结器就像任何普通实例方法一样被声明。
实际上,垃圾回收器调用终结器的时间取决于 JVM 的实现和系统的条件,这是我们无法控制的。
为了使垃圾收集在现场进行,我们将利用System.gc方法。在现实世界的系统中,我们永远不应该显式调用它,原因有很多:
- 成本很高
- 它不会立即触发垃圾回收——它只是 JVM 启动 GC 的提示
- JVM更清楚何时需要调用GC
如果我们需要强制 GC,我们可以使用 jconsole 来实现。
下面是一个演示终结器操作的测试用例:
在第一个语句中,创建一个 Finalizable 对象,然后调用其 readFirstLine 方法。此对象未分配给任何变量,因此在调用 System.gc 方法时,它符合垃圾回收条件。
测试中的断言验证输入文件的内容,仅用于证明我们的自定义类按预期工作。
当我们运行提供的测试时,控制台上将打印一条消息,说明缓冲读取器在终结器中关闭。这意味着调用了 finalize 方法,并且它已清理资源。
到目前为止,终结器看起来像是预销毁操作的好方法。然而,这并不完全正确。
在下一节中,我们将了解为什么应避免使用它们。
尽管它们带来了好处,但终结器也有很多缺点。
让我们来看看使用终结器执行关键操作时将面临的几个问题。
第一个值得注意的问题是缺乏及时性。我们无法知道终结器何时运行,因为垃圾收集可能随时发生。
就其本身而言,这不是问题,因为终结器迟早会执行。但是,系统资源不是无限的。因此,我们可能会在清理发生之前耗尽资源,这可能会导致系统崩溃。
终结器也会对程序的可移植性产生影响。由于垃圾回收算法依赖于 JVM 实现,因此程序可以在一个系统上运行良好,而在另一个系统上的行为却不同。
性能成本是终结器带来的另一个重要问题。具体来说,JVM 在构造和销毁包含非空终结器的对象时必须执行更多操作。
我们要讨论的最后一个问题是在最终确定期间缺乏异常处理。如果终结器引发异常,则终结过程将停止,使对象处于损坏状态,没有任何通知。
是时候把理论放在一边,看看终结者在实践中的效果了。
让我们定义一个带有非空终结器的新类:
请注意 finalize() 方法 – 它只是将一个空字符串打印到控制台。如果此方法完全为空,JVM 会将对象视为没有终结器。因此,我们需要为 finalize() 提供一个实现,在这种情况下它几乎什么都不做。
在 main 方法中,在 for 循环的每次迭代中都会创建一个新的 CrashedFinalizable 实例。此实例未分配给任何变量,因此符合垃圾回收条件。
让我们在标有 // 其他代码的行中添加一些语句,以查看运行时内存中存在多少对象:
给定的语句访问内部 JVM 类中的一些字段,并在每百万次迭代后打印出对象引用的数量。
让我们通过执行 main 方法启动程序。我们可能期望它无限期运行,但事实并非如此。几分钟后,我们应该看到系统崩溃并出现类似于以下内容的错误:
看起来垃圾收集器没有很好地完成它的工作——对象的数量不断增加,直到系统崩溃。
如果我们删除终结器,引用的数量通常为 0,程序将永远运行。
为了理解为什么垃圾回收器没有像它应该的那样丢弃对象,我们需要看看 JVM 在内部是如何工作的。
当创建具有终结器的对象(也称为引用对象)时,JVM 会创建一个类型为 java.lang.ref.Finalizer 的随附引用对象。在引用对象准备好进行垃圾回收后,JVM 将引用对象标记为准备好进行处理,并将其放入引用队列中。
我们可以通过 java.lang.ref.Finalizer 类中的静态字段队列访问此队列。
同时,一个名为 Finalizer 的特殊守护进程线程继续运行,并在引用队列中查找对象。当它找到一个时,它会从队列中删除引用对象,并在引用对象上调用终结器。
在下一个垃圾回收周期中,当引用对象不再引用引用时,引用对象将被丢弃。
如果线程不断高速生成对象(在我们的示例中就是这种情况),则 Finalizer 线程无法跟上。最终,内存将无法存储所有对象,我们最终会出现 OutOfMemoryError。
请注意,以本节所示的速度创建对象的情况在现实生活中并不常见。然而,它展示了一个重要的观点——终结器非常昂贵。
让我们探索一个提供相同功能但不使用 finalize() 方法的解决方案。请注意,下面的示例并不是替换终结器的唯一方法。
相反,它被用来演示一个重要的观点:总有一些选项可以帮助我们避免终结器。
以下是我们新类的声明:
不难看出,新的 CloseableResource 类和我们以前的 Finalizable 类之间的唯一区别是 AutoCloseable 接口的实现,而不是终结器定义。
请注意,CloseableResource 的 close 方法的主体与类 Finalizable 中终结器的主体几乎相同。
下面是一个测试方法,该方法读取输入文件并在完成其作业后释放资源:
在上面的测试中,在 try-with-resources 语句的 try 块中创建了一个 CloseableResource 实例,因此当 try-with-resources 块完成执行时,该资源会自动关闭。
运行给定的测试方法,我们将看到从 CloseableResource 类的 close 方法打印出的消息。
在本教程中,我们重点介绍了 Java 中的一个核心概念——finalize 方法。这在纸面上看起来很有用,但在运行时可能会产生难看的副作用。而且,更重要的是,我们拥有可使用终结器的替代解决方案。
另需要注意的一个关键点:从Java 9开始,finalize已被弃用,最终将被删除。
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.bianchenghao6.com/java-jiao-cheng/17813.html