面试官:说说你对finalize方法的理解
答:它是Object中的一个方法,子类重写他,垃圾回收时此方法将会被调用,可以在其中进行一些资源的释放和清理工作,但是将资源释放和清理放在finalize方法中非常不好,非常影响性能,严重的时候甚至会引起OOM,从Java9开始就被被标注为@Deprecated,不建议被使用了。
面试官:为什么?
答:当重写了finalize方法的对象,在类的构造方法调用之时,JVM都会将其包装成一个Finalizer对象,并加入unfinalized 队列中(静态成员变量、双向链表结构)。同时这些类也会指向一个ReferenceQueue类型,它也是 Finalizer 类中一个静态成员变量,名为queue(是一个单向链表结构),刚开始它是空的。当对象可以被当作垃圾回收时,就会把这些对象对应的Finalizer对象加入这个队列。即使这些对象没人引用,垃圾回收的时候也无法立即被回收,因为要使得重写了finalize方法的类被回收的时候能够调用finalize方法(因为如果先被回收了,那么就没办法调用finalize方法了),那么此时就需要一个FinalizerThread线程,它会去从ReferenceQueue中逐一取出每个Finalizer对象,并把他们从链表断开,这样就没有引用能引用到他了,那么此时下次gc的时候这个对象就可以被回收了。而回收这个对象是通过Finalizer这个守护线程进行回收的。
并且如果在调用finalize方法的时候出现了异常,是不会报错的,这就会导致后期如果代码出错了,很难排查。
答:因此我做一下总结:重写finalize有两个非常不好的点。
- FinalizerThread是守护线程,代码很有可能没来得及执行完,线程就结束了,造成资源没有正确释放
- 异常被吞掉这个就太糟了,甚至不能判断有没有在释放资源时发生错误
其次就是影响性能,重写了 finalize方法的对象在第一次被gc时,并不能及时释放它占用的内存,因为要等着FinalizerThread调用完finalize,把它从第一个unfinalized 队列移除后,第二次gc时才能真正释放内存。
同时可以想象gc本就因为内存不足引起,finalize调用又很慢(两个队列的移除操作,都是串行执行的,用来释放连接类的资源也应该不快),不能及时释放内存,对象释放不及时就会逐渐移入老年代,老年代垃圾积累过多就会容易full gc,full gc后释放速度如果仍跟不上创建新对象的速度,就会OOM。
当然,有一些文章提到【Finalizer线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的CPU时间较少,因此它永远也赶不上主线程的步伐】这个显然是错误的,FinalizerThread的优先级较普通线程更高,赶不上步伐的原因应该是finalize 执行慢等原因综合导致的。
面试官:6