Java中finalize方法使用指南-Java快速入门教程
1. 概述
在本教程中,我们将重点介绍 Java 语言的核心方面 - 根 Object 类提供的 finalize 方法。
简而言之,这是在特定对象的垃圾回收之前调用的。
2. 使用终结器
finalize() 方法称为终结器。
当 JVM 确定此特定实例应该被垃圾回收时,将调用终结器。此类终结器可以执行任何操作,包括使对象恢复生命状态。
但是,终结器的主要目的是在对象从内存中删除之前释放对象使用的资源。终结器可以用作清理操作的主要机制,也可以在其他方法失败时用作安全网。
为了理解终结器的工作原理,让我们看一下类声明:
public class Finalizable { private BufferedReader reader; public Finalizable() { InputStream input = this.getClass() .getClassLoader() .getResourceAsStream("file.txt"); this.reader = new BufferedReader(new InputStreamReader(input)); } public String readFirstLine() throws IOException { String firstLine = reader.readLine(); return firstLine; } // other class members }
类 Finalizable 有一个字段读取器,该读取器引用可关闭的资源。从此类创建对象时,它会构造一个新的 BufferedReader 实例,该实例从类路径中的文件读取。
在 readFirstLine 方法中使用这样的实例来提取给定文件中的第一行。请注意,读取器在给定的代码中未关闭。
我们可以使用终结器来做到这一点:
@Override public void finalize() { try { reader.close(); System.out.println("Closed BufferedReader in the finalizer"); } catch (IOException e) { // ... } }
很容易看出,终结器就像任何普通实例方法一样被声明。
实际上,垃圾回收器调用终结器的时间取决于 JVM 的实现和系统的条件,这是我们无法控制的。
为了使垃圾收集在现场进行,我们将利用System.gc方法。在现实世界的系统中,我们永远不应该显式调用它,原因有很多:
- 成本很高
- 它不会立即触发垃圾回收——它只是 JVM 启动 GC 的提示
- JVM更清楚何时需要调用GC
如果我们需要强制 GC,我们可以使用 jconsole 来实现。
下面是一个演示终结器操作的测试用例:
@Test public void whenGC_thenFinalizerExecuted() throws IOException { String firstLine = new Finalizable().readFirstLine(); assertEquals("jack.yang.com", firstLine); System.gc(); }
在第一个语句中,创建一个 Finalizable 对象,然后调用其 readFirstLine 方法。此对象未分配给任何变量,因此在调用 System.gc 方法时,它符合垃圾回收条件。
测试中的断言验证输入文件的内容,仅用于证明我们的自定义类按预期工作。
当我们运行提供的测试时,控制台上将打印一条消息,说明缓冲读取器在终结器中关闭。这意味着调用了 finalize 方法,并且它已清理资源。
到目前为止,终结器看起来像是预销毁操作的好方法。然而,这并不完全正确。
在下一节中,我们将了解为什么应避免使用它们。
3. 避免终结器
尽管它们带来了好处,但终结器也有很多缺点。
3.1. 终结器的缺点
让我们来看看使用终结器执行关键操作时将面临的几个问题。
第一个值得注意的问题是缺乏及时性。我们无法知道终结器何时运行,因为垃圾收集可能随时发生。
就其本身而言,这不是问题,因为终结器迟早会执行。但是,系统资源不是无限的。因此,我们可能会在清理发生之前耗尽资源,这可能会导致系统崩溃。
终结器也会对程序的可移植性产生影响。由于垃圾回收算法依赖于 JVM 实现,因此程序可以在一个系统上运行良好,而在另一个系统上的行为却不同。
性能成本是终结器带来的另一个重要问题。具体来说,JVM 在构造和销毁包含非空终结器的对象时必须执行更多操作。
我们要讨论的最后一个问题是在最终确定期间缺乏异常处理。如果终结器引发异常,则终结过程将停止,使对象处于损坏状态,没有任何通知。
3.2. 终结器效果演示
是时候把理论放在一边,看看终结者在实践中的效果了。
让我们定义一个带有非空终结器的新类:
public class CrashedFinalizable { public static void main(String[] args) throws ReflectiveOperationException { for (int i = 0; ; i++) { new CrashedFinalizable(); // other code } } @Override protected void finalize() { System.out.print(""); } }
请注意 finalize() 方法 – 它只是将一个空字符串打印到控制台。如果此方法完全为空,JVM 会将对象视为没有终结器。因此,我们需要为 finalize() 提供一个实现,在这种情况下它几乎什么都不做。
在 main 方法中,在 for 循环的每次迭代中都会创建一个新的 CrashedFinalizable 实例。此实例未分配给任何变量,因此符合垃圾回收条件。
让我们在标有 // 其他代码的行中添加一些语句,以查看运行时内存中存在多少对象:
if ((i % 1000000) == 0) { Class<?> finalizerClass = Class.forName("java.lang.ref.Finalizer"); Field queueStaticField = finalizerClass.getDeclaredField("queue"); queueStaticField.setAccessible(true); ReferenceQueue<Object> referenceQueue = (ReferenceQueue) queueStaticField.get(null); Field queueLengthField = ReferenceQueue.class.getDeclaredField("queueLength"); queueLengthField.setAccessible(true); long queueLength = (long) queueLengthField.get(referenceQueue); System.out.format("There are %d references in the queue%n", queueLength); }
给定的语句访问内部 JVM 类中的一些字段,并在每百万次迭代后打印出对象引用的数量。
让我们通过执行 main 方法启动程序。我们可能期望它无限期运行,但事实并非如此。几分钟后,我们应该看到系统崩溃并出现类似于以下内容的错误:
...
There are 21914844 references in the queue
There are 22858923 references in the queue
There are 24202629 references in the queue
There are 24621725 references in the queue
There are 25410983 references in the queue
There are 26231621 references in the queue
There are 26975913 references in the queue
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.lang.ref.Finalizer.register(Finalizer.java:91)
at java.lang.Object.<init>(Object.java:37)
at com.jack.yang.finalize.CrashedFinalizable.<init>(CrashedFinalizable.java:6)
at com.jack.yang.finalize.CrashedFinalizable.main(CrashedFinalizable.java:9)
Process finished with exit code 1
看起来垃圾收集器没有很好地完成它的工作——对象的数量不断增加,直到系统崩溃。
如果我们删除终结器,引用的数量通常为 0,程序将永远运行。
3.3. 解释
为了理解为什么垃圾回收器没有像它应该的那样丢弃对象,我们需要看看 JVM 在内部是如何工作的。
当创建具有终结器的对象(也称为引用对象)时,JVM 会创建一个类型为 java.lang.ref.Finalizer 的随附引用对象。在引用对象准备好进行垃圾回收后,JVM 将引用对象标记为准备好进行处理,并将其放入引用队列中。
我们可以通过 java.lang.ref.Finalizer 类中的静态字段队列访问此队列。
同时,一个名为 Finalizer 的特殊守护进程线程继续运行,并在引用队列中查找对象。当它找到一个时,它会从队列中删除引用对象,并在引用对象上调用终结器。
在下一个垃圾回收周期中,当引用对象不再引用引用时,引用对象将被丢弃。
如果线程不断高速生成对象(在我们的示例中就是这种情况),则 Finalizer 线程无法跟上。最终,内存将无法存储所有对象,我们最终会出现 OutOfMemoryError。
请注意,以本节所示的速度创建对象的情况在现实生活中并不常见。然而,它展示了一个重要的观点——终结器非常昂贵。
4. 无终结器示例
让我们探索一个提供相同功能但不使用 finalize() 方法的解决方案。请注意,下面的示例并不是替换终结器的唯一方法。
相反,它被用来演示一个重要的观点:总有一些选项可以帮助我们避免终结器。
以下是我们新类的声明:
public class CloseableResource implements AutoCloseable { private BufferedReader reader; public CloseableResource() { InputStream input = this.getClass() .getClassLoader() .getResourceAsStream("file.txt"); reader = new BufferedReader(new InputStreamReader(input)); } public String readFirstLine() throws IOException { String firstLine = reader.readLine(); return firstLine; } @Override public void close() { try { reader.close(); System.out.println("Closed BufferedReader in the close method"); } catch (IOException e) { // handle exception } } }
不难看出,新的 CloseableResource 类和我们以前的 Finalizable 类之间的唯一区别是 AutoCloseable 接口的实现,而不是终结器定义。
请注意,CloseableResource 的 close 方法的主体与类 Finalizable 中终结器的主体几乎相同。
下面是一个测试方法,该方法读取输入文件并在完成其作业后释放资源:
@Test public void whenTryWResourcesExits_thenResourceClosed() throws IOException { try (CloseableResource resource = new CloseableResource()) { String firstLine = resource.readFirstLine(); assertEquals("jack.yang.com", firstLine); } }
在上面的测试中,在 try-with-resources 语句的 try 块中创建了一个 CloseableResource 实例,因此当 try-with-resources 块完成执行时,该资源会自动关闭。
运行给定的测试方法,我们将看到从 CloseableResource 类的 close 方法打印出的消息。
5.结论
在本教程中,我们重点介绍了 Java 中的一个核心概念——finalize 方法。这在纸面上看起来很有用,但在运行时可能会产生难看的副作用。而且,更重要的是,我们拥有可使用终结器的替代解决方案。
另需要注意的一个关键点:从Java 9开始,finalize已被弃用,最终将被删除。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix