Java 9+与Scala中Cleanable无法被System.gc()触发的原因及解决
Cleaner清理函数未被System.gc()触发的原因及解决方法
问题场景
在Scala 2.13.12与Java 11环境下,使用java.lang.ref.Cleaner实现对象自动清理逻辑,测试中System.gc()无法触发清理动作,但改用已过时的finalize()方法时测试可正常通过。
测试代码
class CleanableSpike extends AnyFunSpec { import CleanableSpike._ it("triggered by GC") { var v = Dummy(inc) v = null System.gc() Thread.sleep(1000) assert(count == 1) } } object CleanableSpike { val jvmCleaner = Cleaner.create() @transient var count = 0 val inc = () => count += 1 case class Dummy(fn: () => Unit) extends AutoCloseable { final private val cleanable = jvmCleaner.register( this, { () => println("\ncleaned\n") fn() } ) override def close(): Unit = cleanable.clean() } }
测试报错信息
org.scalatest.exceptions.TestFailedException: 0 did not equal 1 at org.scalatest.Assertions.newAssertionFailedException(Assertions.scala:472) at org.scalatest.Assertions.newAssertionFailedException$(Assertions.scala:471) at org.scalatest.Assertions$.newAssertionFailedException(Assertions.scala:1231) at org.scalatest.Assertions$AssertionsHelper.macroAssert(Assertions.scala:1295) at com.tribbloids.spookystuff.lifespan.CleanableSpike.$anonfun$new$1(CleanableSpike.scala:19)
原因分析
核心问题是清理逻辑的lambda表达式意外捕获了Dummy实例的强引用,形成循环引用导致对象无法被GC回收:
- Scala中匿名lambda默认会捕获外部类的
this(即当前Dummy实例),而Cleaner注册后返回的Cleanable对象会持有该lambda的引用。 - 最终形成循环强引用链:
Dummy→Cleanable→ lambda →Dummy,GC无法识别该链上的对象为可回收状态,因此不会触发清理动作。 finalize()是对象自身方法,不存在额外强引用依赖,所以GC可以正常触发。
解决方法
方法一:用弱引用包裹清理逻辑依赖对象,避免捕获this
将清理函数用WeakReference包裹,切断lambda对Dummy实例的强引用:
object CleanableSpike { val jvmCleaner = Cleaner.create() @transient var count = 0 val inc = () => count += 1 case class Dummy(fn: () => Unit) extends AutoCloseable { // 用弱引用持有清理函数,避免捕获外部类实例 private val weakFn = new WeakReference(fn) final private val cleanable = jvmCleaner.register( this, new Runnable { override def run(): Unit = { println("\ncleaned\n") weakFn.get().foreach(_()) } } ) override def close(): Unit = cleanable.clean() } }
方法二:将清理逻辑抽离为静态方法,彻底解耦与目标对象的引用
把清理逻辑封装成独立静态方法,确保清理动作不持有目标对象的强引用:
object CleanableSpike { val jvmCleaner = Cleaner.create() @transient var count = 0 val inc = () => count += 1 // 静态清理逻辑,不依赖Dummy实例 private def createCleanupAction(fn: () => Unit): Runnable = { val weakFn = new WeakReference(fn) () => { println("\ncleaned\n") weakFn.get().foreach(_()) } } case class Dummy(fn: () => Unit) extends AutoCloseable { final private val cleanable = jvmCleaner.register( this, createCleanupAction(fn) ) override def close(): Unit = cleanable.clean() } }
测试补充注意事项
System.gc()仅为JVM提供垃圾回收建议,不保证立即执行,测试时可配合System.runFinalization()提升触发概率。- 确保测试代码中没有其他地方持有
Dummy实例的强引用,否则GC同样无法回收目标对象。
内容的提问来源于stack exchange,提问作者tribbloid
相关产品推荐
相关产品推荐

