转载

从一个 Young GC 变慢的案例来聊聊 finalize 方法

背景

有一次一位同学上线之后,发现Young GC的时间飙升很多,监控如下图:

从一个 Young GC 变慢的案例来聊聊 finalize 方法

监控显示老代码(04机器)的平均young gc时间之后23ms,而新代码(01机器)为平均时间84ms。

上线去查看gc  log,新代码的gc log如下:

从一个 Young GC 变慢的案例来聊聊 finalize 方法

老代码的gc log 如下:

从一个 Young GC 变慢的案例来聊聊 finalize 方法

从上图截图可以发现:新上线的代码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方法的呢?

  1. JVM在加载类的时候,会去识别该类是否实现了 finalize方法并且该方法体不会空;若是含有有意义的 finalize方法体会标记出该类为“ finalize Class”。

  2. 在new “finalize Class”对象时,会调用 Finalizer.register方法,在该方法中new 一个Finalizer对象, Finalizer对象 会引用原始对象,然后把 Finalizer对象注册到 Finalizer对象链里(这样就可以保证 Finalizer对象一直可达的 )。具体代码如下:

    从一个 Young GC 变慢的案例来聊聊 finalize 方法

    当然这步可以使用RegisterFinalizersAtInit这个JVM参数改变注册到 Finalizer对象链中的时机。因为new 一个对象至少分为两步:1.分配内存空间、2.调用构造函数。RegisterFinalizersAtInit默认是true,也就是这两步都完成之后再注册到 Finalizer对象链;如果改成false,会在分配内存完成之后调用构造函数之前注册到 Finalizer对象链中。

  3. 在发生gc的时候,在判断原始对象除了 Finalizer对象引用之外,没有其他对象引用之后,就把 Finalizer对象从对象链中取出,加入到 Finalizer queue队列中。

  4. JVM在启动时,会创建一个“ finalize ”线程,该线程会一直从“ Finalizer   queue ”队列中取出对象,然后执行原始对象中的 finalize方法。

    从一个 Young GC 变慢的案例来聊聊 finalize 方法

    从一个 Young GC 变慢的案例来聊聊 finalize 方法

    从一个 Young GC 变慢的案例来聊聊 finalize 方法

  5. 在完成步骤4中, Finalizer对象以及其引用的原始对象,再也没有其他对象引用他们,属于不可达对象,再次GC的时候他们将会被回收掉。(如果在 finalize方法重新使该对象再次可达,再次GC该对象也不会被回收 )。

使用finalize方法带来哪些影响?

  1. 创建一个包含finalize方法的对象时,需要额外创建 Finalizer对象并且注册到 Finalizer 对象链中;这样就需要额外的内存空间,并且创建 finalize方法的对象的时间要长。 笔者在本机上测试, 创建 普通对象和 含finalize方法的对象 时间相差4倍左右 (循环10000创建一个不含任何变量的对象)。

  2. 和相比普通对象,含有 finalize方法的对象的生存周期变长,普通对象一次GC就可以回收;而 含有finalize方法的对象至少需要两次gc,这样就会导致young gc阶段Object Copy阶段时间上升

  3. 在gc时需要对 包含finalize方法的对象做特殊处理,比如识别该对象是否只有 Finalizer对象引用,把 Finalizer对象添加到 queue队列这些都是在gc阶段完成,需要额外处理时间,在young gc属于 Ref Proc时间,必然导致 Ref Proc阶段时间上升。

  4. 因为 finalize ”线程优先级比较低, 如果cpu比较繁忙,可能会导致 queue队列有挤压,在经历多次young gc之后 原始对象和 Finalizer对象就会进入 old区域,那么这些对象只能等待old gc才能被释放掉。

总结

使用finalize()方法本身会加重系统负担、严重影响GC并且不能保证 finalize的调用时机等一系列问题。所以 对于 普通的程序开发人员还是忘记有该方法的存在 吧。他的应用场景也仅仅针对于防止资源泄漏等场景,但是如果仅仅内部调用也不需要实现 finalize()方法

原文  https://mp.weixin.qq.com/s/rQ64IayuoVte6aJEVcaowQ
正文到此结束
Loading...