此系列文章为本人对《Effective Java》一书的学习笔记,主要是记录对书中重点内容的理解。
既然有缘看到此文,那么希望能对你有所帮助。
本文对应原书第8条 避免使用终结方法和清除方法
什么是finalize方法
finalize方法又称终结方法(finalizer)
,打开Object
类的源码,在不起眼的最底部,可以找到带着一大串注释的,没有任何逻辑代码的finalize()
:
protected void finalize() throws Throwable { }
结合《Java编程思想》和《深入理解Java虚拟机》,我对于这个方法的理解如下:
- 首先这个方法是要自己去重写的,在GC准备清除掉某个对象时,会首先调用它;
- 但由于GC的自动回收机制的不确定性,不能保证它会执行(比如从头到尾没GC过),更别说及时执行,并且即使执行也不能保证执行完(万一你再这个方法里面搞个大循环甚至是死循环呢);
- 这个方法可以救活要被清理的对象,但只能救一次,因为这个方法最多被执行一次(可以参考《深入理解Java虚拟机》中的例子)。
《Effective Java》给出了一个总结性的描述:
终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。
所以在Java 9中用清除方法(cleaner)
代替了终结方法
。(但是由于对于java版本,我们一直秉承 你发任你发,我用java8
的理念,暂时过多不去探讨清除方法
)。
为什么避免使用finalize方法
1. 不稳定
根本不能保证会被执行,更别说时效性,所以根本靠不住,如果你的代码里有注重时间(time-critical)
的任务,还是别交给它了。
举个栗子:你用它去关闭已打开的文件,这就是一个严重的错误,你的这个程序很可能会让大量的文件保留在打开的状态,其间后果,可想而知。
2. 性能损失
由于终结方法会阻止有效的垃圾回收,所以导致性能会出现明显的损失(比如我正在抄作业,没有终结方法的话可以一气呵成,有了终结方法就像是有的字不认识,得单独拎出来查个字典啥的,必然就慢了)。
按照原书作者的测试,使用
终结方法
去创健和销毁对象的速度大概是try-with-resource
速度的2%,如果仅仅使用终结方法
去做一个安全网
(下面会提,别着急),速度也只能达到正常速度的20%。
3. 安全隐患
终结方法为终结方法攻击(finalizer attack)
打开了类的大门。
终结方法攻击(finalizer attack):
如果从构造器或者它的序列化对等体(readObject 和 readResolve方法)抛出异常,恶意子类的终结方法就可以在构造了一部分的应该已经半途夭折的对象上运行。这个终结方法会将对该对象的引用记录在一个静态域中,阻止它被垃圾回收。一旦记录到异常的对象,就可以轻松地在这个对象上调用任何原本永远不允许在这里出现的方法。从构造器抛出的异常应该足以防止对象继续存在;有了终结方法的存在,这一点就做不到了。这种攻击可能造成致命的后果。
finalize方法真的一无是处?
存在即合理(虽然后来被代替了),作用有这样几条:
- 做实验:在你研究JVM的时候,用终结方法可以做不少实验,让你受益匪浅;
- 安全网:假如资源的所有者忘了调
close
方法,那么它可以当安全网兜个底(但得考虑清楚,这样的兜底是否对得起性能损失的代价); - 回收本地对等体(但对资源关闭和性能有要求的话也不可以使用);
安全网举例,
FileInputStream
:
本地对等体是一个
本地对象(native object)
,普通对象通过本地方法(native method)
委托给一个本地对象。因为本地对等体不是一个普通对象,所以垃圾回收器不会知道它,当它的Java对等体被回收的时候,它不会被回收。
如果本地对等体没有关键资源,并且性能也可以接受的话,那么清除方法或者终结方法正式执行这项任务最合适的工具。
如果本地对等体拥有必须被及时终止的资源,或者性能无法接受,那么该类就应该具有一个close方法。
总结
如果是java 9+的版本(finalize的升级版----清除方法),除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用。
如果是java 8(finalize方法),那么还是拉倒吧。
但若真的用上了,请一定注意它的不确定性和性能后果。
水平有限,若文章中存在错误,恳请不吝赐教,这对我以及后面的读者都有重要意义