讨论Java中的可终结对象通常会讨论当最终化对象(及其相关资源)无法快速被垃圾收集时发生的常见间接成本。
目前,我更感兴趣的是,无论是在内存方面还是在对象分配时间内,最终可实现的直接成本是多少。我已经在很多地方看到了对这种费用存在的倾斜性提法,例如,Oracle's article on finalization memory retention issues注:
分配
obj
时,JVM会在内部记录obj
是否可以终止。这通常会减慢现代JVM所具有的快速分配路径。
JVM如何记录对象实例的最终结果,以及这样做的内存和性能成本是多少?
对于那些对我的具体应用感兴趣的人:
我们生产和保留了数以百万计的轻巧物体;向这些对象添加单个指针是非常昂贵的,因此我们已经做了相当多的工作来从它们中删除指针,而是使用较小的数字ID打包到字段的一个子集中。解包该数字允许从使用Map存储它们的池中检索具有该id的共享不可变属性。
剩下的问题是如何处理不再使用的属性值的垃圾收集。
已经考虑的一个策略是使用引用计数;当创建对象并检索值的池化id时,该值的引用计数递增;当它不再使用时,必须递减。
确保发生此减量的一个选项是添加以下finalize方法:
public void finalize() {
Pool.release(getPropertyId());
}
但是,如果可最终化的行为意味着必须保留指向该对象的附加指针,那么对于该应用程序而言,可最终确定的前期成本将被认为是高的。如果这意味着必须分配额外的对象,那几乎肯定会过高......因此,我的问题是:最终化的直接前期成本是多少?
答案 0 :(得分:8)
终结器糟糕不仅是因为保留问题,而且也是从性能角度来看。
在Oracle JDK / OpenJDK中,具有attach()
方法的对象由Finalizer的实例支持,finalize
是java.lang.ref.Reference
的子类。
所有终结器都在对象的构造函数的末尾注册,分两步:调用from Java to VM,然后调用Finalizer.register()。 JIT编译器无法内联这种双转换Java-> VM-> Java。但最糟糕的是,Finalizer的构造函数在global lock下创建了一个链表! (捂脸)
终结器在内存占用方面也很糟糕:除了所有参考字段外,它们还有two extra fields:next
和prev
。
PhantomReferences比终结者好得多:
java.lang.ref.Reference
; This benchmark比较可终结对象的分配速度和PhantomReference支持的对象:
Benchmark Mode Cnt Score Error Units
Finalizer.finalizable thrpt 5 2171,312 ± 1469,705 ops/ms
Finalizer.phantom thrpt 5 61280,612 ± 692,922 ops/ms
Finalizer.plain thrpt 5 225752,307 ± 7618,304 ops/ms