Java 中方法的众多问题finalize
之一是“对象复活”问题(在此问题中解释):如果一个对象已完成,并且它保存了全局可访问的某个位置的副本this
,则对该对象的引用“转义”并且你结束有一个最终确定的但有生命的对象(不会再次最终确定,否则会出现问题)。
为了避免创建复活的对象,通常的建议(例如,在这个答案中看到)是创建对象的新实例,而不是保存对象本身;这通常可以通过将所有对象的字段复制到一个新对象中来完成。在大多数情况下,这实现了允许原始对象被释放而不是复活的目标。
但是,Java 垃圾收集器支持引用循环的垃圾收集;这意味着一个对象可以在(直接或间接)包含对自身的引用时完成,并且两个对象可以在(直接或间接)包含对彼此的引用时完成。在这种情况下,“将所有字段复制到一个新对象”建议实际上并不能解决问题。虽然我们在终结器完成运行后丢弃this
引用,但部分终结的对象将通过来自字段的引用复活。因此,无论如何我们最终都会使对象复活。
在对象间接持有对自身的引用的情况下,可以递归地查看对象的所有字段,直到找到自引用(在这种情况下,我们可以将其替换为对新对象的引用建造),从而防止复活。这样就解决了这种情况下的问题。
然而,如果两个对象持有对彼此的引用(因此两者同时被释放),并且我们正在为每个对象创建一个新实例,那么每个新对象都将持有对旧的最终对象的引用(而不是作为替代构造的新对象)。这显然是一种不良状态,所以我一直在研究的一件事是尝试使用与单对象情况相同的解决方案:递归扫描(活的、新构造的)对象的字段以查找最终对象,并用相应的替换对象替换它们。
问题是:当我这样做时,如何识别最终/复活的对象?显而易见的方法是在终结器中以某种方式记录最终对象的身份,然后将我们在递归扫描期间找到的所有对象与最终对象列表进行比较。问题是,似乎没有有效的方法来记录相关对象的身份:
- 常规(强)引用将使对象保持活动状态,有效地自动复活它,并且不提供任何方法来确定对象实际上没有被引用。这将解决识别复活对象的问题,但也有其自身的问题:虽然复活的对象将永远不会被使用,除了它们的身份之外,没有任何方法可以释放它们(例如,你不能使用 a
PhantomReference
来检测对象现在真的死了,就像在 Java 中通常那样,因为对象现在是强可达的,因此幻像引用永远不会清除)。所以这实际上意味着有问题的对象永远保持分配状态,从而导致内存泄漏。 - 使用弱引用是我的第一个想法,但有一个问题是,在我们构造
WeakReference
对象时,被引用的对象实际上不是强、软、弱可达的。因此,一旦我们存储了WeakReference
可强到达的任何位置(以防止WeakReference
自身被释放),WeakReference
的目标就变得弱可到达并且引用自动清除。所以我们不能以这种方式存储任何信息。 - 使用幻像引用的问题是,无法将幻像引用与对象进行比较以查看该引用是否引用了该对象。(也许应该有 - 不像
get()
,它可以复活一个对象,这个操作从来没有任何危险,因为我们显然有一个对该对象的引用 - 但它在 Java API 中不存在。同样,.equals()
在PhantomReference
对象上是==
,不是值相等,因此您不能使用它来确定两个幻像引用是否引用同一事物。) - 使用
System.identityHashCode()
记录对应于对象身份的数字几乎可以工作 - 对象的释放不会改变记录的数字,数字不会阻止对象的释放,并且复活对象会使值保持不变 - 但不幸的是,作为一个hashCode
,它容易发生碰撞,因此可能会出现误报,其中一个对象似乎复活了,但实际上并没有。 - 最后一种可能性是修改对象本身以将其标记为已完成(并跟踪其替换的位置),这意味着在一个强可达对象上观察此标记将显示它是一个复活的对象,但这需要添加一个额外的字段来引用循环中可能涉及的任何对象。
总而言之,我的根本问题是“给定一个当前正在完成的对象,安全地创建它的副本,而不会意外地复活可能在该过程中它的引用循环中的任何对象”。我一直在尝试使用的方法是“当一个可能参与循环的对象最终确定时,跟踪该对象的身份,以便随后可以用它的副本替换它,如果它证明可以从另一个最终确定的对象”;但上述五种方法似乎都不令人满意。
是否有其他方法可以跟踪最终对象,以便在意外重定向时可以识别它们?对于原始问题是否有完全不同的解决方案,即在对象完成过程中安全地复制对象?