背景
有一次一位同学上线之后,发现Young GC的时间飙升很多,监控如下图:
监控显示老代码(04机器)的平均young gc时间之后23ms,而新代码(01机器)为平均时间84ms。
上线去查看gc log,新代码的gc log如下:
老代码的gc log 如下:
从上图截图可以发现:新上线的代码Object Copy阶段时间上升了20ms左右,Ref Proc时间上升了45ms。导致整个young gc时间上升了60ms+。
把新上线的机器上的堆内存dump下来,使用MTA打开之后,发现有很多java.lang.ref.Finalizer对象,这个对象引用了业务对象。查看这个业务对象发现他实现了Object中的finalize方法,删除 finalize方法上线之后,young gc恢复正常。
那么为什么在一个对象中加入 finalize方法之后,young gc时间会长这么多,并且是消耗在 Copy阶段和 Ref Proc阶段。
finalize方法如何影响 GC执行的
在Object中有对 finalize方法如何工作的做出了说明,可以转述为:“ 在子类实现了 finalize方法时,当 垃圾回收器确定该对象没有任何引用时,就会调用 finalize方法,并且finalize方法最多被调用一次”。
JVM在加载类的时候,会去识别该类是否实现了 finalize方法并且该方法体不会空;若是含有有意义的 finalize方法体会标记出该类为“ finalize Class”。
在new “finalize Class”对象时,会调用 Finalizer.register方法,在该方法中new 一个Finalizer对象, Finalizer对象 会引用原始对象,然后把 Finalizer对象注册到 Finalizer对象链里(这样就可以保证 Finalizer对象一直可达的 )。具体代码如下:
当然这步可以使用RegisterFinalizersAtInit这个JVM参数改变注册到 Finalizer对象链中的时机。因为new 一个对象至少分为两步:1.分配内存空间、2.调用构造函数。RegisterFinalizersAtInit默认是true,也就是这两步都完成之后再注册到 Finalizer对象链;如果改成false,会在分配内存完成之后调用构造函数之前注册到 Finalizer对象链中。
在发生gc的时候,在判断原始对象除了 Finalizer对象引用之外,没有其他对象引用之后,就把 Finalizer对象从对象链中取出,加入到 Finalizer queue队列中。
JVM在启动时,会创建一个“ finalize ”线程,该线程会一直从“ Finalizer queue ”队列中取出对象,然后执行原始对象中的 finalize方法。
在完成步骤4中, Finalizer对象以及其引用的原始对象,再也没有其他对象引用他们,属于不可达对象,再次GC的时候他们将会被回收掉。(如果在 finalize方法重新使该对象再次可达,再次GC该对象也不会被回收 )。
使用finalize方法带来哪些影响?
创建一个包含finalize方法的对象时,需要额外创建 Finalizer对象并且注册到 Finalizer 对象链中;这样就需要额外的内存空间,并且创建 finalize方法的对象的时间要长。 笔者在本机上测试, 创建 普通对象和 含finalize方法的对象 时间相差4倍左右 (循环10000创建一个不含任何变量的对象)。
和相比普通对象,含有 finalize方法的对象的生存周期变长,普通对象一次GC就可以回收;而 含有finalize方法的对象至少需要两次gc,这样就会导致young gc阶段Object Copy阶段时间上升 。
在gc时需要对 包含finalize方法的对象做特殊处理,比如识别该对象是否只有 Finalizer对象引用,把 Finalizer对象添加到 queue队列这些都是在gc阶段完成,需要额外处理时间,在young gc属于 Ref Proc时间,必然导致 Ref Proc阶段时间上升。
因为 “ finalize ”线程优先级比较低, 如果cpu比较繁忙,可能会导致 queue队列有挤压,在经历多次young gc之后 原始对象和 Finalizer对象就会进入 old区域,那么这些对象只能等待old gc才能被释放掉。
使用finalize()方法本身会加重系统负担、严重影响GC并且不能保证 finalize的调用时机等一系列问题。所以 对于 普通的程序开发人员还是忘记有该方法的存在 吧。他的应用场景也仅仅针对于防止资源泄漏等场景,但是如果仅仅内部调用也不需要实现 finalize()方法 。