前言

本文隶属于专栏《100个问题搞定Java并发》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!

本专栏目录结构和参考文献请见100个问题搞定Java并发

正文

内存清理

程序员都了解初始化的重要性,但通常会忽略清理的重要性。

毕竟,谁会去清理一个 int 呢?但是使用完一个对象就不管它并非总是安全的。

Java 中有垃圾回收器回收无用对象占用的内存。

但现在考虑一种特殊情况:

你创建的对象不是通过 new 来分配内存的,而垃圾回收器只知道如何释放用 new 创建的对象的内存,所以它不知道如何回收不是 new 分配的内存

为了处理这种情况, Java 允许在类中定义一个名为 finalize()的方法。

finalize 的工作原理

当垃圾回收器准备回收对象的内存时,首先会调用其 finalize()方法,并在下一轮的垃圾回收动作发生时,才会真正回收对象占用的内存。

所以如果你打算使用 finalize(),就能在垃圾回收时做一些重要的清理工作。

finalize 的问题

在 Java 中,对象并非总是被垃圾回收,或者换句话说

  1. 对象可能不被垃圾回收
  2. 垃圾回收不等同于析构

这意味着在你不再需要某个对象之前,如果必须执行某些动作,你得自己去做。

Java 没有析构器或类似的概念,所以你必须得自己创建一个普通的方法完成这项清理工作。

例如,对象在创建的过程中会将自己绘制到屏幕上。

如果不是明确地从屏幕上将其擦除,它可能永远得不到清理。

如果在 finalize() 方法中加入某种擦除功能,那么当垃圾回收发生时, finalize() 方法被调用(不保证一定会发生),图像就会被擦除,要是"垃圾回收"没有发生,图像则仍会保留下来。

也许你会发现,只要程序没有濒临内存用完的那一刻,对象占用的空间就总也得不到释放

如果程序执行结束,而垃圾回收器一直没有释放你创建的任何对象的内存,则当程序退出时,那些资源会全部交还给操作系统。

这个策略是怡当的,因为垃圾回收本身也有开销,要是不使用它,那就不用支付这部分开销了。

finalize 存在的价值

如果你不能将 finalize()作为通用的清理方法,那么这个方法有什么用呢?

垃圾回收只与内存有关。

也就是说,使用垃圾回收的唯一原因就是为了回收程序不再使用的内存。

所以对于与垃圾回收有关的任何行为来说(尤其是 finalize() 方法),它们也必须同内存及其回收有关。

但这是否意味着如果对象中包括其他对象, finalize()方法就应该明确释放那些对象呢?

不是,无论对象是如何创建的,垃圾回收器都会负责释放对象所占用的所有内存。

这就将对 finalize()的需求限制到一种特殊情况,即通过某种创建对象方式之外的方式为对象分配了存储空间

不过,你可能会想, Java 中万物皆对象,这种情况怎么可能发生?

看起来之所以有 finalize() 方法,是因为在分配内存时可能采用了类似 C 语言中的做法,而非 Java 中的通常做法。

这种情况主要发生在使用本地方法的情况下

关于本地方法请参考我的这篇博客——JNI的实现原理是什么?

读到这里,你可能明白了不会过多使用 finalize()方法。

对,它确实不是进行普通的清理工作的合适场所。

那么,普通的清理工作在哪里执行呢?。

你必须实施清理要清理一个对象,用户必须在需要清理的时候调用执行清理动作的方法。

这听上去相当直接,但却与 C++ 中的"析构函数"的概念稍有抵触。

在 C++ 中,所有对象都会被销毁,或者说应该被销毀。

如果在 C++ 中创建了一个局部对象(在栈上创建,在 Java 中不行),此时的销毁动作发生在以"右花括号"为边界的、此对象作用域的末尾处。

如果对象是用 new 创建的(类似于 Java 中),那么当程序员调用 C++ 的 delete 操作符时( Java 中不存在),就会调用相应的析构函数。

如果程序员忘记调用 delete ,那么永远不会调用析构函数,这样就会导致內存泄露,对象的其他部分也不会得到清理。

这种 bug 很难跟踪,也是让 C++ 程序员转向 Java 的一个主要因素。

相反,在 Java 中,没有用于释放对象的 delete ,因为垃圾回收器会帮助你释放存储空间。

甚至可以肤浅地认为,正是由于垃圾回收的存在,使得 Java 没有析构函数。

然而,随着学习的深入,你会明白垃圾回收器的存在并不能完全替代析构函数(而且绝对不能直接调用 finalize(),所以这也不是一种解決方案)。

如果希望进行除释放存储空间之外的清理工作,还是得明确调用某个恰当的 Java 方法:这就等同于使用析构函数了,只是没有它方便。

记住,无论是"垃圾回收"还是"终结",都不保证一定会发生

如果 Java 虚拟机( JVM )并未面临内存耗尽的情形,它可能不会浪费时间执行垃圾回收以恢复内存

通常,不能指望 finalize(),你必须创建其他的"清理"方法,并明确地调用它们。

所以看起来, finalize() 只对大部分程序员很难用到的一些晦涩內存清理里有用。

但是, finalize()还有一个有趣的用法,它不依赖于每次都要对 finalize() 进行调用,这就是对象终结条件的验证

当对某个对象不感兴趣时ー一一也就是它将被清理了,这个对象应该处于某种状态,这种状态下它占用的内存可以被安全地释放掉

例如,如果对象代表了一个打开的文件,在对象被垃圾回收之前程序员应该关闭这个文件。

只要对象中存在没有被适当清理的部分,程序就存在很隐晦的 bug 。

finalize() 可以用来最终发现这个情況,尽管它并不总是被调用。

如果某次 finalize() 的动作使得 bug 被发现,那么就可以据此找出问题所在一一这才是人们真正关心的。

finalize 与资源管理

当不再需要内存资源时,可以通过垃圾回收器来回收它们,但对于其他一些资源,例如文件句柄或套接字句柄,当不再需要它们时,必须显式地交还给操作系统。

为了实现这个功能,垃圾回收器对那些定义了 finalize 方法的对象会进行特殊处理:在回收器释放它们后,调用它们的 finalize 方法,从而保证一些持久化的资源被释放。

由于终结器可以在某个由 JVM 管理的线程中运行,因此终结器访问的任何状态都可能被多个线程访问,这样就必须对其访问操作进行同步。

终结器并不能保证它们将在何时运行甚至是否会运行,并且复杂的终结器通常还会在对象上产生巨大的性能开销。

要编写正确的终结器是非常困难的。

在大多数情况下,通过使用 try-with-resources 能够比使用终结器更好地管理资源。

唯一的例外情况在于:当需要管理对象,并且该对象持有的资源是通过本地方法获得的。

上一篇 下一篇