1

Java 中方法的众多问题finalize之一是“对象复活”问题(在此问题中解释):如果一个对象已完成,并且它保存了全局可访问的某个位置的副本this,则对该对象的引用“转义”并且你结束有一个最终确定的但有生命的对象(不会再次最终确定,否则会出现问题)。

为了避免创建复活的对象,通常的建议(例如,在这个答案中看到)是创建对象的新实例,而不是保存对象本身;这通常可以通过将所有对象的字段复制到一个新对象中来完成。在大多数情况下,这实现了允许原始对象被释放而不是复活的目标。

但是,Java 垃圾收集器支持引用循环的垃圾收集;这意味着一个对象可以在(直接或间接)包含对自身的引用时完成,并且两个对象可以在(直接或间接)包含对彼此的引用时完成。在这种情况下,“将所有字段复制到一个新对象”建议实际上并不能解决问题。虽然我们在终结器完成运行后丢弃this引用,但部分终结的对象将通过来自字段的引用复活。因此,无论如何我们最终都会使对象复活。

在对象间接持有对自身的引用的情况下,可以递归地查看对象的所有字段,直到找到自引用(在这种情况下,我们可以将其替换为对新对象的引用建造),从而防止复活。这样就解决了这种情况下的问题。

然而,如果两个对象持有对彼此的引用(因此两者同时被释放),并且我们正在为每个对象创建一个新实例,那么每个新对象都将持有对旧的最终对象的引用(而不是作为替代构造的新对象)。这显然是一种不良状态,所以我一直在研究的一件事是尝试使用与单对象情况相同的解决方案:递归扫描(活的、新构造的)对象的字段以查找最终对象,并用相应的替换对象替换它们。

问题是:当我这样做时,如何识别最终/复活的对象?显而易见的方法是在终结器中以某种方式记录最终对象的身份,然后将我们在递归扫描期间找到的所有对象与最终对象列表进行比较。问题是,似乎没有有效的方法来记录相关对象的身份:

  • 常规(强)引用将使对象保持活动状态,有效地自动复活它,并且不提供任何方法来确定对象实际上没有被引用。这将解决识别复活对象的问题,但也有其自身的问题:虽然复活的对象将永远不会被使用,除了它们的身份之外,没有任何方法可以释放它们(例如,你不能使用 aPhantomReference来检测对象现在真的死了,就像在 Java 中通常那样,因为对象现在是强可达的,因此幻像引用永远不会清除)。所以这实际上意味着有问题的对象永远保持分配状态,从而导致内存泄漏。
  • 使用弱引用是我的第一个想法,但有一个问题是,在我们构造WeakReference对象时,被引用的对象实际上不是强、软、弱可达的。因此,一旦我们存储了WeakReference可强到达的任何位置(以防止WeakReference自身被释放),WeakReference的目标就变得弱可到达并且引用自动清除。所以我们不能以这种方式存储任何信息。
  • 使用幻像引用的问题是,无法将幻像引用与对象进行比较以查看该引用是否引用了该对象。(也许应该有 - 不像get(),它可以复活一个对象,这个操作从来没有任何危险,因为我们显然有一个对该对象的引用 - 但它在 Java API 中不存在。同样,.equals()PhantomReference对象上是==,不是值相等,因此您不能使用它来确定两个幻像引用是否引用同一事物。)
  • 使用System.identityHashCode()记录对应于对象身份的数字几乎可以工作 - 对象的释放不会改变记录的数字,数字不会阻止对象的释放,并且复活对象会使值保持不变 - 但不幸的是,作为一个hashCode,它容易发生碰撞,因此可能会出现误报,其中一个对象似乎复活了,但实际上并没有。
  • 最后一种可能性是修改对象本身以将其标记为已完成(并跟踪其替换的位置),这意味着在一个强可达对象上观察此标记将显示它是一个复活的对象,但这需要添加一个额外的字段来引用循环中可能涉及的任何对象。

总而言之,我的根本问题是“给定一个当前正在完成的对象,安全地创建它的副本,而不会意外地复活可能在该过程中它的引用循环中的任何对象”。我一直在尝试使用的方法是“当一个可能参与循环的对象最终确定时,跟踪该对象的身份,以便随后可以用它的副本替换它,如果它证明可以从另一个最终确定的对象”;但上述五种方法似乎都不令人满意。

是否有其他方法可以跟踪最终对象,以便在意外重定向时可以识别它们?对于原始问题是否有完全不同的解决方案,即在对象完成过程中安全地复制对象?

4

2 回答 2

3

为了避免创建复活的对象,通常的建议(例如,在这个答案中看到)是创建对象的新实例,而不是保存对象本身;这通常可以通过将所有对象的字段复制到一个新对象中来完成。

这不是“正常建议”,甚至链接的答案也没有声称。链接的答案以“<em>如果你绝对必须复活对象,......</em>”开头,这清楚地表明这不是关于如何“避免创建复活对象”的建议。

该答案中描述的方法对象复活,具有讽刺意味的是,这正是您要解决的问题所描述的场景,即另一个对象的终结器对对象(通过复制的字段引用的对象)的复活。

这保留了与终结器和对象复活相关的所有问题,但只有一个问题。它解决的唯一问题是最终确定的对象不会再次最终确定,这是最小的问题。

当应用程序放弃一个对象时,它不必处于有效状态。对象仅在打算再次使用时才需要保持在有效状态。例如,应用程序在处理完资源后调用close()代表资源的对象是正常的。但是当发生错误时,在操作中间放弃一个对象也是合理的。错误的结果状态可以由不同的对象表示,并且不使用另一个现在不一致的对象。

终结器必须处理所有这些可能的对象状态,甚至更糟糕的是,终结器导致的不可用对象状态。正如您所认识到的那样,对象图可能会作为一个整体被收集起来,并且它们的所有终结器都会以任意顺序甚至同时执行。所以它不需要循环,也不需要复活尝试陷入麻烦。当对象 A 具有对对象 B 的引用并且都具有终结器时,清理 A 的尝试可能会在过程中需要 B 时失败,因为 B 可能已经终结,甚至处于并发终结的中间。

简而言之,最终化甚至不适合它最初打算进行的清理。这就是finalize()Java 9 弃用该方法的原因。

您尝试重用正在完成的对象的字段值只是在火上浇油。想想上面的 A→B 场景。当 A 的终结器将字段值复制到另一个对象时,这意味着复制对 B 的引用,并且不需要 B 的终结器尝试执行相同操作。如果 B 的终结器完成其预期的工作,清理相关资源,从而使 B 处于不可用状态,这就足够了。

总而言之,我的根本问题是“给定一个当前正在完成的对象,安全地创建它的副本,而不会意外地复活可能在该过程中它的引用循环中的任何对象”。

如前所述,“当前正在完成的对象”和“安全”本身就是矛盾的。它不需要相互尝试重用来破坏它。即使只看你最初的狭隘问题陈述,你所有的方法都有一个问题,他们甚至没有试图阻止这个问题。他们都只是在事后的某个任意时间尝试检测问题。

也就是说,将 a 的所指对象WeakReference与其他强引用(如weakReference.get() == someStrongReference. 弱引用仅在所指对象被垃圾回收时才会被清除,这意味着强引用不可能指向它,因此将引用与之false进行比较的答案将是正确的答案。nullsomeStrongReference

于 2019-03-26T12:49:10.523 回答
0

正如其他答案所表明的那样,尝试以这种方式解决根本问题是无法完成的,在尝试解决此类问题时需要进行更广泛的重新思考。这篇文章描述了我用来解决问题的解决方案,以及我是如何做到的。

假设目标是“跟踪对象在它变得未被引用时的样子”,这只能在对象本身没有终结器时安全地完成(否则,会有许多难以解决的问题,如问题、评论和其他答案中所述)。我们在这里实际上需要终结器的唯一原因是我们无法在对象变得未引用后获取该对象。

让对象变为未引用然后从其终结器中恢复它显然是一个坏主意。然而,“恢复”没有终结器的对象的问题要小得多(因为这相当于该对象根本不会被释放——它不会像带有终结器的对象那样最终“部分终结”)。这可以通过创建一个带有终结器的单独对象来实现,并有意在原始对象和单独的、带有终结器的对象之间创建一个引用循环(它只有一个终结器和对原始对象的引用 t,仅此而已);当对象以其他方式未被引用时,新对象上的终结器将运行,但原始对象不会被释放并且不会

当然,终结器必须打破循环(将自己从原始对象中移除),以避免自己复活;如果在终结期间创建了对原始对象的新强引用(取消其解除分配),则终结对象将因此必须用新的终结对象替换自己(但这很容易做到,因为它不携带状态,有只有一个引用,我们知道该对象在哪里)。

总之:没有安全的方法可以在对象自己的终结期间保持活动状态,即使您将其所有字段复制到其他地方也不行:相反,您需要确保对象没有终结器,而是使用其他对象的定稿。

于 2019-03-26T18:21:16.840 回答