是否可以从其终结器跟踪对象,以检测其他对象的终结器对对象的意外复活?

时间:2019-03-26 01:30:53

标签: java garbage-collection finalizer reference-cycle

Java中finalize方法的众多问题之一是“对象复活”问题(在this question中进行了解释):如果对象已完成,并且保存了{{1} }在全局可访问的某个地方,对对象的引用“转义”,您最终得到了一个已定型但仍处于活动状态的对象(该对象将不会再次被定型,否则将是一个问题)。

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

但是,Java垃圾收集器支持参考周期的垃圾收集。这意味着一个对象可以在(直接或间接)包含对自身的引用时完成,而两个对象可以在(直接或间接)包含彼此的引用时完成。在这种情况下,“将所有字段复制到新对象中”建议实际上并不能解决问题。尽管一旦终结器完成运行,我们将丢弃this引用,但部分终结的对象将通过字段中的引用来恢复。因此,我们最终还是将对象复活了。

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

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

问题是:执行此操作时,如何识别完成/复活的对象?这样做的明显方法是在终结器中以某种方式记录终结对象的身份,然后将在递归扫描期间找到的所有对象与终结对象列表进行比较。问题是,似乎没有一种有效的方式来记录有关对象的身份:

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

总而言之,我的根本问题是“给定一个当前正在完成的对象,安全地创建它的副本,而又不会意外地在该过程的参考周期中复活任何对象”。我一直在尝试使用的方法是“当一个可能涉及循环的对象最终确定时,请跟踪该对象的身份,以便在后来发现它可以从另一个对象访问时,可以用其副本替换它。最终对象”;但是上述五种方法都不令人满意。

是否还有其他方法可以跟踪已完成的对象,以便在意外重定向时可以将其识别出来?对于原始问题,是否存在一种完全不同的解决方案,那就是在最终确定对象时安全地对其进行复制?

2 个答案:

答案 0 :(得分:3)

  

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

这不是“正常建议”,甚至连答案也没有。链接的答案以“ 如果您绝对必须复活对象,... ”开头,这很清楚这不是 关于如何“避免 的建议。 em>创建复活的物体”。

答案中描述的方法是对象的复活,具有讽刺意味的是,这就是场景,您将其描述为您要解决的问题,即对象的复活(那些通过复制字段引用的内容)由另一个对象的终结器进行。

这保留了除终结器和对象复活以外的所有问题。它解决的唯一问题是,最终对象不会再被最终确定,这是最小的问题。

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

终结器将不得不处理所有这些可能的对象状态,甚至更糟糕的是,终结器将导致无法使用的对象状态。如您所知,对象图可能会作为一个整体被收集,并且所有终结器都将以任意顺序甚至并行执行。因此,它不需要循环,也不需要进行复活尝试即可陷入麻烦。当对象A引用对象B并且都具有终结器时,在过程中需要B时清理A的尝试可能会失败,因为B可能已经被终结,或者甚至在并发终结中。

简而言之,终结处理甚至不适合其最初打算进行的清理。这就是finalize()方法在Java 9中已被弃用的原因。

您试图在最终确定状态下重用对象的字段值只是在增加火焰。只需考虑上面的A→B情况。当A的终结器将字段值复制到另一个对象时,这意味着将引用复制到B,并且不需要B的终结器尝试执行此操作。如果B的终结器完成了预期的工作,清理了相关资源,从而使B处于无法使用的状态,就已经足够了。

  

总而言之,我的根本问题是“考虑到当前正在完成的对象,可以安全地创建它的副本,而不会意外地在该过程的参考周期中复活任何对象”。

如前所述,“当前正在最终确定的对象”和“安全”本身就是一个矛盾。不需要相互重用就可以打破它。即使仅查看原始的狭义问题陈述,您所有的方法都存在一个问题,即它们甚至都没有试图阻止该问题。他们都只尝试在事实发生后的任意任意时间发现问题。

也就是说,将WeakReference的引用对象与其他强引用(例如weakReference.get() == someStrongReference)进行比较没有问题。仅当弱引用被垃圾回收时才清除,这意味着强引用不可能指向它,因此将false引用与{{1 }}将是正确的答案。

答案 1 :(得分:0)

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

假设目标是“跟踪对象在未引用时的外观”,那么只有在对象本身没有终结器时才能安全地完成此操作(否则,有很多难以实现的目标)解决问题,如问题,其评论和其他答案中所述)。我们在这里实际上需要终结器的唯一原因是,在该对象成为未引用对象之后,我们否则无法到达该对象。

让对象成为未引用对象然后从其终结器中恢复显然是一个坏主意。但是,“恢复”没有终结器的对象的问题要少得多(因为这等效于根本不会释放该对象–它不会像带有终结器的对象那样最终“部分终结”)。这可以通过以下方式完成:创建带有终结器的单独对象,并有意在原始对象与单独的带有终结器的对象之间创建一个引用循环(该对象具有 just 终结器和对t的引用t原始对象,别无其他);当该对象成为其他情况下的未引用对象时,新对象上的终结器将运行,但是原始对象不会被释放,并且不会以任何尴尬的终结相关状态结束。

为了避免使自身复活,终结器当然必须中断循环(将自身从原始对象中删除);如果在完成过程中创建了对原始对象的新强引用(取消其释放),则完成对象将因此必须用新的完成对象来代替自身(但这很容易做到,因为它不携带状态,因此只有一个引用,我们知道该对象在哪里。

结论:没有安全的方法可以使对象在其自身的终结过程中保持活动状态,即使您将其所有字段复制到其他地方也是如此:相反,您需要确保该对象没有终结器,而应使用其他对象的终结处理。

相关问题