关于finalize(),System.gc()和C#、C++的析构函数的一点笔记
2018-4-2 补充:
在阅读了深入理解JVM后,作者的这一段话彻底理清了这一块:
“需要说明的是,上面关于对象死亡时finalize()方法的描述(前文描述了在finalize()方法中让对象重新被引用,从而可以避免一次垃圾回收)可能带有悲情的艺术色彩,…笔者建议大家避免使用它,…而是Java刚诞生时为了使C/C++程序员更容易接受它做出的妥协。它运行代价高昂,不确定性太大,无法保证各个对象的调用顺序,有些教材描述它适合用来关闭外部资源,这完全是对这个方法用途的一种自我安慰,finalize()能做的,finally块等其他方式能做的更好、更及时,所以笔者建议大家完全可以忘掉Java中有这个方法的存在。”
这个方法实质是一种妥协,缺点见下文。
先说finalize。
在我之前的个人理解里,finalize方法并不会直接导致该对象被释放/回收;因此它不是C++意义上的析构函数。
(事实上析构函数的作用是定义对象被释放时候的行为,是对析构函数的调用,而不是析构函数本身导致对象被释放)
它用于 标记在对象被回收时应该做一些事情;例如释放其他资源,关闭连接等。
以下说明摘抄自1.6的API文档:
当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。子类重写 finalize 方法,以配置系统资源或执行其他清除。
finalize 的常规协定是:当 Java虚拟机已确定尚未终止的任何线程无法再通过任何方法访问此对象时,将调用此方法,除非由于准备终止的其他某个对象或类的终结操作执行了某个操作。finalize 方法可以采取任何操作,其中包括再次使此对象对其他线程可用;不过,finalize 的主要目的是在不可撤消地丢弃对象之前执行清除操作。例如,表示输入/输出连接的对象的 finalize 方法可执行显式 I/O 事务,以便在永久丢弃对象之前中断连接。
Object 类的 finalize 方法执行非特殊性操作;它仅执行一些常规返回。Object 的子类可以重写此定义。
Java 编程语言不保证哪个线程将调用某个给定对象的 finalize 方法。但可以保证在调用 finalize 时,调用 finalize 的线程将不会持有任何用户可见的同步锁定。如果 finalize 方法抛出未捕获的异常,那么该异常将被忽略,并且该对象的终结操作将终止。
在启用某个对象的 finalize 方法后,将不会执行进一步操作,直到 Java 虚拟机再次确定尚未终止的任何线程无法再通过任何方法访问此对象,其中包括由准备终止的其他对象或类执行的可能操作,在执行该操作时,对象可能被丢弃。
对于任何给定对象,Java 虚拟机最多只调用一次 finalize 方法。
finalize 方法抛出的任何异常都会导致此对象的终结操作停止,但可以通过其他方法忽略它。
在JDK9的API中,该方法已经被废弃:
已过时。 finalize机制本质上是有问题的。finalize可能导致性能问题,死锁和挂起。
finalize方法中的错误可能导致资源泄漏; 如果 finalization不再需要,无法取消 ; 并且在不同对象的finalize方法的调用中没有指定排序。 此外,finalization的时间并不能得到保证。finalize方法可能只能在无限期的延迟之后,才调用到可终结的对象上。
如果一个类的实例持有非堆资源,那么它应提供一种方法,来实现这些资源的显式释放,如果适用,它们还应实现AutoCloseable 。
Cleaner和PhantomReference在对象变得不可达时提供更灵活和更有效的方式来释放资源。
API Note:
嵌入非堆资源的类,具有许多清除这些资源的选项。 该类必须确保每个实例的生命周期 比其嵌入的任何资源的寿命都要长。
当嵌入在对象中的资源正在使用时,可以使用Reference.reachabilityFence(java.lang.Object)来确保对象保持可访问。
一个子类应该避免覆盖finalize方法,除非子类嵌入在收集实例之前必须清理的非堆资源。
与构造函数不同,调用子类的finalize()不会自动调用父类的finalize()方法。如果一个子类覆盖了finalize,必须明确地调用超类终结器。
为了防止异常提前终止终结链,子类应该使用一个try-finally块来确保总是调用super.finalize() 。 例如:
1 |
|
结论:在1.9之后,我们不应该用这个方法来实现 让一个类被回收时做某些事 的功能。如果需要做某些事,应当显式的提供做这些事的方法,例如实现AutoCloseable接口,用虚引用和Cleaner等新版的功能。
再说System.gc()。
从入门开始,我便被各种书籍告知,不要手动调用System.gc()。那么这个方法到底做了什么呢?是立即发起垃圾回收,还是催促虚拟机进行一次垃圾回收呢?
看看API文档:
调用gc方法表明,Java虚拟机花费了回收未使用对象的努力,以使其当前占用的内存可用于快速重用。
当控件从方法调用返回时,Java虚拟机已经尽力从所有丢弃的对象中回收空间。
不知道具体JVM的实现是什么,至少这段话我的理解就是 立即发起一次垃圾回收……
由于垃圾回收会停止主程序活动,而且的确实际开发中也没有理由手动发起垃圾回收,总之还是不要用的比较好(
联动:C#的析构函数
学习finalize的时候,往往会同时提起析构函数这一C系语言的产物。
但是在C#中,析构函数和finalize的作用,在我的理解中是一致的:即 让这个类在被回收前做一些事情。
不过C#的析构函数并没有被废弃,微软同时建议使用IDispose接口,实现Dispose()方法。
不过和Java的AutoCloseable不同,Dispose方法需要被重载两次,第一次是表示该对象不执行析构函数:
1 | public void Dispose() |
第二次才是真正的执行逻辑,此时的重载是Dispose(bool),如果对它方法的调用是来自Dispose(),则参数为true;如果来自析构函数,则参数为false。因此需要对参数进行判断:当调用来自Dispose()时,释放非托管资源。
联动:C++的析构函数
水平有限,不做拓展.
大致可以看出也是在对象生命周期结束时被自动调用,但是由于没有自动内存管理,C++的析构函数会在如下几种情况被调用:
- 使用 delete 运算符 显式解除 分配了使用 new 运算符分配的对象。 使用 delete 运算符解除分配对象时,将为“大多数派生对象” 或 “属于完整对象,但不是表示基类的子对象的对象”释放内存。 此“大多数派生对象”解除分配一定仅对虚拟析构函数有效。 在类型信息与实际对象的基础类型不匹配的多重继承情况下,取消分配可能失败。
- 具有块范围的本地(自动)对象超出范围。
- 临时对象的生存期结束。
- 程序结束,并且存在全局或静态对象。
- 使用析构函数的完全限定名显式调用了析构函数。
关于finalize(),System.gc()和C#、C++的析构函数的一点笔记