闲逛ITEye时看到了译帝的一篇翻译博客,其中提到了关于Java类重写finalize方法后带来的诡异的GC overhead limit问题。博客的结尾非常详细的说明了这个问题产生的原理,但是始终有一个地方没有得到清晰的答案:由于finalize方法是Object类的protected方法,即无论重写与否,所有的Java类都会带有finalize方法,但为什么只有重写之后才会出现GC问题,不重写与重写的真实差别到底在哪儿?
通过思考始终得不到答案,索性打开Eclipse直接调试代码:
首先验证了博客中的现象的确可以重现。
中间一度怀疑问题的原因可能是finalize方法中引用了类的静态变量AtomicInteger会引起GC的问题(实际上这个方向是错误的,GC是根据对象是否存在被引用关系来判断对象是否回收,惭愧),删掉原来的方法体然后随意在finalize方法中声明了一个变量,结果运行时问题仍然存在。
到了此时,开始怀疑是不是和finalize方法体有关系了,直接删除掉finalize方法体中的所有内容,运行后发现问题真的不存在了!
由于出现GC问题时内存堆中存在大量Finalizer对象,而Finalizer对象只能通过Finalizer类的静态register方法创建,因此尝试在register方法中加上断点对两种情况分别运行,发现方法体为空时,register方法不会被调用,这样自定义对象便没有被任何对象引用,可以轻松的被GC回收掉。
由于register方法是被VM所调用,只能求助于莫枢大神,而莫枢也确认了只要Java类以及它所有的祖先类中不含有finalize方法或者finalize方法体为空时,VM便不会将该类的对象实例注册为finalizable对象(地址)。
至此,算是把整个问题的来龙去脉给整理的差不多了:
JVM创建自定义对象。
JVM检测对象类以及祖先类是否含有非空的finalize方法定义,如果均没有,则不进行后续的Finalizer相关处理。
JVM调用Finalizer类的静态register方法,创建包含该对象引用的Finalizer对象,同时Finalizer静态类将该Finalizer对象加入到一个单向链表中。
运行过程中,JVM会将这些Finalizer对象的状态更新为pending(这部分可以参考java.lang.ref.Reference类中的说明)。
Reference类中的ReferenceHandler线程会扫描到这些状态为pending的Finalizer对象,将这些对象enqueue到Finalizer静态类引用的ReferenceQueue(非Finalizer中的单向链表)当中。
Finalizer线程从ReferenceQueue中逐一弹出Finalizer对象,首先将Finalizer对象从Finalizer的单向链表中删除,解除了Finalizer静态类对Finalizer对象的引用关系,之后调用Finalizer对象引用的自定义对象的finalize方法,Finalizer对象以及自定义对象此时均可被GC回收。
由于Finalizer线程的低优先级,可能引起旧对象的释放速度无法跟上新对象的创建速度,引起OutOfMemory问题(例子中的GC overhead limit原因是由于新对象创建的代价太低而旧对象回收的代价较高导致CPU用于GC回收的时间比例超过98%)
最后,网易研究院的马进在他的博客中非常详细的阐述了finalize的原理以及因此引发的案例,也非常值得一读。