真没想到(java finalizer)java中final的意思,Java的finalizer,cleaner等如何实现?,java.lang.finalizer,
先看假设:
0:JVM GC的主流实现是基于tracing的对的。现代主流JVM的GC的基础算法都是从tracing系的mark-sweep / mark-compact / copying演化出来的。
1:tracing并不对每个unreachable Object续个释放,而是(在一定的处理后)对一个region的所有Object进行释放对的。Tracing GC的效率的来源就是它有些耗时的阶段只跟活对象的状况相关,而死去的对象直接被这些阶段忽略(尤其是copying GC,整个GC过程的开销都只跟活对象有关而跟死对象无关),所以如果等一段时间再batch起来处理的话,那些在这个时间段内死去了的对象就不会产生收集开销,就省去了像引用计数那样需要时刻记录对象图的局部变化来跟踪中间状态的开销。这样,只要能控制tracing系GC的触发频率低(意味着head room要大),它的工作效率就可以非常高。
2:所以,当内存足够大的时候,tracing系GC可能比stack allocation/手动 free更快(见Garbage Collection Can Be Faster Than Stack Allocation)唔…现实地说tracing GC要比优化好的stack allocation快还是很困难的,或者说只能在很受限的场景有这种效果。如果head room大的话,要比朴素的手动malloc/free快有可能,要在多线程条件下比朴素引用计数throughput更高那简直易如反掌。
然后回答问题:
0:Java为什么有finalizer(尽管不可靠)?只是作为last line of defense,是一种保底的措施。由于GC只能管理自动内存资源而无法管理程序所需要的各种其它资源(例如GC堆外的native memory、file handle、socket、draw device handle啥的),程序员必须要自己手动来管理这些资源。只是纯粹为了避免在对象死了之后它原本持有的这样的资源泄漏,Java才提供了finalizer机制到让用户注册 finalize() 这么个回调方法来定制GC清理对象的行为。
确实Java语言规范里是说底层实现可以自由选择当对象可以被回收之后何时调用 finalize() ;换句话说在无限远的未来才调用 finalize() 也是符合规范的,现实来说就是等价于永远不调用 finalize() 也是符合规范的。但靠谱的JVM实现都还是会在维持GC效率的前提下尽快对可以回收的对象去调用 finalize() 方法。
1:有没有任何办法保证finalizer(或类似机制)在一定条件满足时(如GC被触发时)必会执行?现实上说是会被执行的,但没办法抽象地、严格地有什么机制去保证 finalize() 一定被调用。
HotSpot VM实现finalizer的办法其实很直观:系统通过特殊的FinalReference(一种介于WeakReference和PhantomReference之间的内部实现的弱引用类型)来引用带有非空 finalize() 方法的对象——这些对象在创建的时候就会伴随创建出一个引用它的FinalReference出来。然后在GC的时候,FinalReference也跟其它弱引用类型一样由ReferenceProcessor发现并处理:GC的marking分为strong marking和weak marking两个阶段,在strong marking过程中如果mark到弱引用的话,并不是立即把其referent也mark上,而是会把弱引用记录在ReferenceProcessor里对应的队列里。Strong marking之后会做reference processing,扫描前面记录下来的弱引用看它们的referent是否已经被strong marking标记为活,如果是的话说明对象还活得好好的,就不管它了让它继续活下去,反之则意味着referent指向的对象只是weak-reachable,就要做相应的弱引用处理。对FinalReference来说,弱引用处理就是这次把referent还是标记为活的,并把它加入到finalize queue里去等着被FinalizeThread去调用其 finalize() 方法。等它的 finalize() 方法被调用过之后,下次再GC的时候这个对象就没有FinalReference引用了,所以就不会再经历一次弱引用处理,就可以好好长眠了。
那么finalizer会有啥问题呢?首先是会慢——GC在做reference processing的时候常常是在stop-the-world pause里做的,会增加GC暂停时间,而且后面跑finalizer也要占CPU;其次是finalizer还是有可能没能被调用上,会泄漏资源。
举个简单的例子,HotSpot VM默认是在一个JVM实例内开一个 FinalizeThread 来专门去调用 finalize() 方法的。所有GC发现可以被回收但有finalizer的对象都会被挂在一个全局的finalize queue上,然后这个FinalizeThread从queue里逐个取出对象来调用其 finalize() 方法。那么假如有个对象在自己的 finalize() 写了个无限循环会怎样?在默认配置下,finalize queue里挂在这个对象之后的对象的 finalize() 方法就要在无限远的未来才能被调用到了——悲剧。
JDK有提供一个标准API,System.runFinalization() ,来让当前Java线程在处理完finalize queue之前block住。这个API的描述也就是“尽力而为”,并不保证任何行为,但现实来说它是会等到finalize queue真的空了才返回的。JDK8u里HotSpot VM的实现方法是调用java.lang.ref.Finalizer.runFinalization(),其中会在 FinalizeThread 之外再开一个 Secondary Finalize Thread 来尝试加速处理finalize queue。那么如果我们的finalize queue上挂着两个调皮的对象都有无限循环,这就还是能把两个处理线程都卡住,于是queue里剩下的对象还是得到无限远的未来才能得到处理…
顺带放俩传送门:
RednaxelaFX:为什么Java有GC还需要自己来关闭某些资源?[Java] 新版本的Java将会废弃Object.finalize(),并添加新的java.lang.ref.Cleaner2:GC Can be Faster Than Stack Allocation在finalizer下是否依然成立?Java程序要想性能好请尽量不要用finalize()嗯。看了上面的实现方式的描述,您觉得这还能比stack allocation快么?>_<
(不过公平地说,C++对象的析构器里要是写了无限循环的话一样悲剧。我们也见过不少有趣的情况是析构器里的逻辑的开销非常大,但因为隐藏在代码里(“}”!),大家写代码的时候未必会一眼看出来,然后就要漫长地通过跑性能测试啊啥的才发现这问题了。这锅要让stack allocation背似乎不合理。同理的话,finalize开销大的锅让GC背似乎也不合理。Anyway这里锅让谁背就见仁见智啦)
本文系作者 @河马 原创发布在河马博客站点。未经许可,禁止转载。
暂无评论数据