You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 22:04:59