Java 有两种不同的机制来响应对象的释放。旧的机制 usingfinalize
会在对象被释放之前运行一个特定的方法。新机制 using允许您在对象被释放后PhantomReference
立即运行特定方法。1
该finalize
技术更强大,因为您可以在对象被this
释放时访问它;但也更危险,因为有可能(有意或无意地)在终结器中创建对对象的新引用。这可以直接完成(例如通过分配this
给静态字段)或间接完成(例如两个对象同时变为未引用,一个引用另一个,因此最终对象最终通过不同对象的字段间接访问终结器)。这种情况下,对象最终确定,但仍然可以从某个地方到达,称为对象复活。虽然它已经定义了语义2,它们在实践中往往是相当有问题的语义,并且通常被视为等同于未定义的行为。
对对象释放做出反应的PhantomReference
方法基本上是一种受约束的终结形式,通过不给你工具来防止你犯任何错误:当你对释放做出反应时,对象已经(有效地)释放了,因此您没有机会意外地复活它或同时释放的任何其他对象。(特别是,PhantomReference
不能访问对象的this
指针;PhantomReference#get
总是返回null
。)幻像引用还有其他优点,例如,API 允许您精确控制终结器在哪个线程上运行以及它当时将要做什么。
那么为什么要使用幻像参考呢?基本上,任何你想对对象的释放做出反应的情况都应该使用PhantomReference
,因为它使用的 API 可以防止终结器的各种常见错误。finalize
(现在已弃用)应仅用于确实没有其他选择的情况。
不幸的是PhantomReference
,尽管 API 比 for 更难误用,但finalize
通常也更难使用:
- 你需要一个对象来容纳它
PhantomReference
自己;只有当这个PhantomReference
对象还活着时才会触发。例如,如果您想在对象死亡时从映射中删除有关对象的元数据,则将PhantomReference
as/within 存储在实现映射的同一对象中的单独字段中是有意义的(这WeakMap
在 Java 中实际上是这样工作的) . 如果您使用终结器来管理像文件句柄这样的全局资源,那么PhantomReference
将需要通过一些全局结构(例如,某个类的静态字段中的集合)来保持活动状态。
- 您需要一个
ReferenceQueue
处理终结器的调度。
- 你需要一个方法来完成你的终结工作——当你监控的对象被释放时运行的东西。
PhantomReference
没有直接规定其中之一;通常的技术是扩展PhantomReference
并为生成的派生类提供相关方法。
- 您需要轮询参考队列;这是指定终结器在哪个线程上运行以及它当时正在做什么的操作。可能性包括使用单独的线程进行参考队列轮询,或者在程序没有做任何重要的事情时(例如,就在它开始阻塞输入之前)使用程序的主线程。
- 轮询引用队列实际上并不运行终结器(毕竟,
PhantomReference
没有直接提供在终结时运行的方法)。相反,轮询引用队列只会为您提供PhantomReference
看到释放的对象。由于PhantomReference
类本身没有对此做出反应的有用方法,因此您需要将其强制转换为适当的类,然后运行您创建的方法。
- 轮询引用队列也不会解除分配
PhantomReference
对象(您必须在其他对象中保持活动状态;否则它将不起作用)。所以当你看到一个幻像引用出列时,如果你想避免内存泄漏,你必须手动将它从保持它活着的东西(通常是一个集合)中删除。
- 如果您需要有关已释放对象的更多信息(例如,在 a 的情况下
WeakMap
,这将是对需要删除的映射条目的引用),您必须将其存储在某处,因为否则它将不可用当对象被释放时。通常,您会将数据存储在PhantomReference
自身中(无论如何您都在使用它的派生类,您可以在派生类中创建字段来存储数据)。请记住,这不能引用您想要对其解除分配做出反应的对象,因为否则您最终会保持对象处于活动状态并且它永远不会被解除分配。
尽管您可以自己处理所有这些复杂性,并且如果您想对幻像引用做一些不寻常的事情,有时是必要的,但使用将所需操作包装成更方便的 API 的预先编写的库更为常见. 例如,在java.lang.ref.Cleaner
内部使用幻像引用来提供更类似于 的 API finalize
,但是(因为它基于幻像引用)可以安全地防止意外复活和类似问题。因此,虽然幻影引用通常对于对无法访问的对象做出反应非常有用,但程序员很少真正直接处理它们。使用在内部使用它们的库会更常见。
1在旧版本的 Java 中,幻像引用技术实际上保留了分配对象的内存,直到幻像引用被清除;但这只是一个实现细节,因为无法访问有问题的内存,并且对象应该被视为已经从幻像引用处理程序中释放,因为无论如何您都无法对它们做任何事情。
2复活的对象保持分配状态,直到它变得无法访问,此时它在不运行其终结器的情况下被释放,除非它被同时释放的其他对象的终结器第二次复活。依赖此行为的代码可能已损坏。