假设对象 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 版本。
假设您有充分的理由这样做,那么
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");
}
}