哪种 JVM 解引用对象清理实现效果最好?

问题描述 投票:0回答:1

假设对象 K 与稀缺的系统资源相关联(例如绑定到本地主机 UDP 上的开放端口,其中每台机器只有 65535 个可用)。 JVM 应用程序需要创建对象、使用资源执行任务,并在请求系统 GC 时释放它。 (显然,将其定义为 AutoClosable 并在 withResource 块中使用它可能是一个更有效的选择,但这不是这个问题的主题)

到 2023 年,任何 JVM 语言都有多种实现可供使用(以 Scala 2.13 为例):


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来触发

  • WeakReference/PhantomReference 成员与活动的 ReferenceQueue 监视线程关联(分别由

    class _4()
    _5()
    说明)。同样,它们也不能通过解引用和系统 GC 来触发,此外,它们都需要一个昂贵的监视线程,该线程在其生命周期的大部分时间内都处于阻塞/休眠状态。目前尚不清楚该线程是否可以替换为低成本的绿色线程或协程。

  • 第三方实现,由于选项较多,没有给出测试代码。一个值得注意的例子是

    com.google.common.base.internal.Finalizer

我不认为这些实现是完美的,甚至是有效的。有没有一种规范的、官方推荐的、经过测试的方法?

更新1:我完全同意在管理系统资源(例如端口或堆外内存)时不应依赖和滥用GC机制。这就是为什么测试套件中显示的清理机制仅在资源已绑定到范围/生命周期的实际情况下使用。并且无论 GC 如何都会被释放(例如,在 JVM 终止的情况下,它们将由系统关闭挂钩释放)。尽管如此,资源仍有可能在其生命周期之前很久就被取消引用和 GC,并且可以实现内存占用的轻微改善。在没有进一步说明的情况下,我们假设问题是关于这些特性/功能的有效用例,而不是资源管理

另外,为了简短,最初的测试套件只是用 Scala 编写的,稍后我会添加 Java 版本。

java scala garbage-collection java-threads finalizer
1个回答
0
投票

假设您有充分的理由这样做,那么

Cleaner
是自 Java 9 以来推荐的方法,如
Object.finalize()

上的 Javadoc 所示

已弃用。最终确定机制本质上是有问题的。最终确定可能会导致性能问题、死锁和挂起。终结器中的错误可能会导致资源泄漏;如果不再需要,则无法取消最终确定;并且不同对象的

finalize
方法的调用之间没有指定顺序。此外,我们无法保证最终确定的时间。
finalize
方法可能仅在无限期延迟后(如果有的话)在可终结对象上调用。实例持有非堆资源的类应该提供一种方法来显式释放这些资源,并且如果合适,它们还应该实现
AutoCloseable
java.lang.ref.Cleaner
java.lang.ref.PhantomReference
提供了更灵活、更高效的方法来在对象无法访问时释放资源

例如,JDK 内部的 NativeBuffer 使用它作为释放本机内存的最后手段。

我不是Scala人,所以无法评论你的Scala代码中发生了什么,这是一个Java示例,注意

System.gc()
是对JVM的提示,它不保证GC会被触发通过调用,因此需要 while 循环和一些字节数组分配来增加内存压力。

import java.lang.ref.Cleaner;

class CleanerExample {

    private static final Cleaner cleaner = Cleaner.create();

    private static volatile boolean cleaned;

    CleanerExample() {
        cleaner.register(this, () -> cleaned = true);
    }

    public static void main(String[] args) throws Exception {
        new CleanerExample();
        while (!cleaned) {
            var waste = new byte[512 * 1024 * 1024];
            System.gc();
            Thread.sleep(1000);
            System.out.println("Waiting to be cleaned...");
        }
        System.out.println("Cleaned");
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.