性能文章>从一个Young GC变慢的案例来聊聊finalize方法>

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

1年前
706517

背景

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

image.png

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

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

image.png

老代码的gc log 如下:

image.png

从上图截图可以发现:新上线的代码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对象一直可达的)。具体代码如下:

image.png

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

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

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

image.png

image.png

image.png

  1. 在完成步骤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()方法。

请先登录,再评论

图片挂了,辛苦补充一下。😁

1年前

为你推荐

不起眼,但是足以让你有收获的JVM内存分析案例
分析 这个问题说白了,就是说有些int[]对象不知道是哪里来的,于是我拿他的例子跑了跑,好像还真有这么回事。点该 dump 文件详情,查看相关的 int[] 数组,点该对象的“被引用对象”,发现所
从一起GC血案谈到反射原理
前言 首先回答一下提问者的问题。这主要是由于存在大量反射而产生的临时类加载器和 ASM 临时生成的类,这些类会被保留在 Metaspace,一旦 Metaspace 即将满的时候,就会触发 Fu
关于内存溢出,咱再聊点有意思的?
概述 上篇文章讲了JVM在GC上的一个设计缺陷,揪出一个导致GC慢慢变长的JVM设计缺陷,可能有不少人还是没怎么看明白的,今天准备讲的大家应该都很容易看明白 本文其实很犹豫写不写,因为感觉没有
协助美团kafka团队定位到的一个JVM Crash问题
概述 有挺长一段时间没写技术文章了,正好这两天美团kafka团队有位小伙伴加了我微信,然后咨询了一个JVM crash的问题,大家对crash的问题都比较无奈,因为没有现场,信息量不多,碰到这类问题我
又发现一个导致JVM物理内存消耗大的Bug(已提交Patch)
概述 最近我们公司在帮一个客户查一个JVM的问题(JDK1.8.0_191-b12),发现一个系统老是被OS Kill掉,是内存泄露导致的。在查的过程中,阴差阳错地发现了JVM另外的一个Bug。这个B
JVM实战:优化我的IDEA GC
IDEA是个好东西,可以说是地球上最好的Java开发工具,但是偶尔也会卡顿,仔细想想IDEA也是Java开发的,会不会和GC有关,于是就有了接下来对IDEA的GC进行调优 IDEA默认JVM参数: -
不起眼,但是足以让你收获的JVM内存案例
今天的这个案例我觉得应该会让你涨姿势吧,不管你对JVM有多熟悉,看到这篇文章,应该还是会有点小惊讶的,不过我觉得这个案例我分享出来,是想表达不管多么奇怪的现象请一定要追究下去,会让你慢慢变得强大起来,
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得