哪种JVM可解除引用对象清理实现方案最优?
假设对象K关联稀缺系统资源(如绑定到本地UDP的开放端口,单台机器仅65535个可用)。JVM应用需创建该对象、使用资源后,在系统触发GC时释放资源(显然,将其定义为AutoClosable并在withResource块中使用是更高效的方案,但不在本次讨论范围内)。
截至2023年,以Scala 2.13为例,JVM语言有多种实现方式:
import org.scalatest.funspec.AnyFunSpec import java.lang.ref.Cleaner import scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future} import scala.ref.{PhantomReference, ReferenceQueue, WeakReference} class GCCleaningSpike extends AnyFunSpec { import GCCleaningSpike._ describe("System.gc() can dispose unreachable object") { it("with finalizer") { var v = Dummies._1() assertInc { v = null } } it("<: class with finalizer") { var v = Dummies._2() assertInc { v = null } } it("registered to a cleaner") { @volatile var v = Dummies._3() assertInc { v = null } } it("registered to a phantom reference cleanup thread") { @volatile var v = Dummies._4() assertInc { v = null } } it("registered to a weak reference cleanup thread") { @volatile var v = Dummies._4() assertInc { v = null } } } } object GCCleaningSpike { implicit lazy val ec: ExecutionContextExecutor = ExecutionContext.global case class WithFinalizer(fn: () => Unit) { case class _1() { override def finalize(): Unit = fn() } trait _2Base { override def finalize(): Unit = fn() } case class _2() extends _2Base final val _cleaner = Cleaner.create() case class _3() extends AutoCloseable { final private val cleanable = _cleaner.register( this, { () => println("\ncleaned\n") fn() } ) override def close(): Unit = cleanable.clean() } lazy val phantomQueue = new ReferenceQueue[_2Base]() case class _4() { val ref = new PhantomReference(this, phantomQueue) } val cleaningPhantom: Future[Unit] = Future { while (true) { val ref = phantomQueue.remove fn() } } lazy val weakQueue = new ReferenceQueue[_2Base]() case class _5() { val ref = new WeakReference(this, weakQueue) } val cleaningWeak: Future[Unit] = Future { while (true) { val ref = weakQueue.remove fn() } } } @transient var count = 0 val doInc: () => Unit = () => count += 1 def assertInc(fn: => Unit): Unit = { val c1 = count fn System.gc() Thread.sleep(1000) val c2 = count assert(c2 - c1 == 1) } object Dummies extends WithFinalizer(doInc) }
各实现的问题点:
- Finalizer方法(由
class _1()演示):测试中有效,但始终不可靠(如JVM进程终止时可能不执行),且已在Java 11中废弃 - 关联引用清理器的Cleanable成员(由
class _3()演示):测试中无法通过解除引用和系统GC触发 - 关联活跃ReferenceQueue监控线程的WeakReference/PhantomReference成员(分别由
class _4()和_5()演示):同样无法通过解除引用和系统GC触发,且需要高成本的监控线程(大部分时间处于阻塞/休眠状态),暂不清楚是否可用低成本绿色线程或协程替代 - 第三方实现:因选项过多未提供测试代码,典型示例为
com.google.common.base.internal.Finalizer
我发现这些实现均不完美甚至无法正常工作,是否存在规范的、官方推荐且经过测试的实现方式?
更新1: 我完全认同不应依赖或滥用GC机制管理系统资源(如端口或堆外内存)。因此测试套件中的清理机制仅用于资源已绑定到特定作用域/生命周期的场景,且无论GC如何都会释放资源(如JVM终止时通过系统关闭钩子释放)。但仍存在资源在生命周期结束前被解除引用并GC的可能,借此可小幅优化内存占用。无需进一步说明,假设本次问题聚焦于这些特性/能力的有效使用场景,而非资源管理本身。
另外,原测试套件仅为简洁性使用Scala编写,后续会补充Java版本。
规范实现方案:Java Cleaner API
官方明确推荐的替代Finalizer的方案是Java 9引入的java.lang.ref.Cleaner,这是经过官方测试、性能和可靠性均优于Finalizer的实现。你测试中_3()的Cleaner未触发,大概率是测试代码存在问题,可从以下几点修正:
确保对象真正不可达
测试中直接在当前方法内将变量置为null,JIT优化可能保留栈上的引用残留。建议将对象的创建和解除引用逻辑放到单独的方法中,确保对象脱离所有强引用:def createAndDiscard(): Unit = { val v = Dummies._3() // 使用对象 } // 在assertInc中调用createAndDiscard(),而非直接在方法内操作变量避免清理逻辑持有强引用
清理逻辑不能持有被清理对象的强引用,否则对象永远无法被GC回收。调整后的_3()实现:final val _cleaner = Cleaner.create() case class _3() extends AutoCloseable { // 用独立Runnable持有清理逻辑,避免对当前对象的强引用 private val cleanable = _cleaner.register(this, new Runnable { override def run(): Unit = { println("\ncleaned by cleaner\n") fn() } }) override def close(): Unit = cleanable.clean() }处理GC的不确定性
System.gc()仅为GC执行建议,不保证立即触发。测试时可配合System.runFinalization(),或添加JVM参数-XX:+ExplicitGCInvokesConcurrent强制触发GC。
PhantomReference的正确使用方式
之前的_4()实现存在自引用问题:对象内部持有自己的PhantomReference,导致对象永远无法被判定为不可达。正确做法是外部持有引用,而非对象自身持有:
lazy val phantomQueue = new ReferenceQueue[AnyRef]() object PhantomCleaner { val cleaningTask: Future[Unit] = Future { while (true) { val ref = phantomQueue.remove().asInstanceOf[PhantomReference[_3]] // 执行清理逻辑 fn() } } def register(obj: AnyRef): Unit = { new PhantomReference(obj, phantomQueue) } } case class _4() { // 外部注册引用,对象自身不持有 PhantomCleaner.register(this) }
总结
- 官方推荐的规范实现是**
java.lang.ref.Cleaner**,它解决了Finalizer的可靠性和性能问题,是当前JVM环境下的最优选择 - 无论使用
Cleaner还是引用队列,都必须确保对象能真正成为不可达状态,且清理逻辑不持有对象的强引用 - 始终配合
AutoCloseable实现,让用户可以主动释放资源,GC清理仅作为兜底机制
内容的提问来源于stack exchange,提问作者tribbloid

