是否有可能从终结器中跟踪对象,以检测不同对象的终结器对对象的意外复活?

问题描述 投票:1回答:2

Java中finalize方法的许多问题之一是“对象复活”问题(在this question中解释):如果一个对象已经完成,并且它保存了一个全局可到达的this副本,那么对象的引用“逃脱”而你最终得到一个最终但活的对象(不会再次敲定,否则会出现问题)。

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

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

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

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

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

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

总而言之,我的根本问题是“给定一个当前正在最终确定的对象,安全地创建它的副本,而不会意外地复活可能在其过程中的参考周期中的任何对象”。我一直试图使用的方法是“当一个可能涉及一个循环的对象被最终确定时,跟踪该对象的身份,以便随后可以将其替换为其副本,如果它可以从另一个对象访问最终目标“;但上述五种方法中没有一种似乎令人满意。

是否有其他方法可以跟踪已完成的对象,以便在意外重定向时可以识别它们?原始问题是否存在完全不同的解决方案,即在最终确定期间安全地制作对象的副本?

java garbage-collection finalizer reference-cycle
2个回答
3
投票

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

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

该答案中描述的方法是一个物体复活,具有讽刺意味的是,正是这个场景,你描述的是你想要解决的问题,一个物体的复活(通过复制的田地引用的那些)被另一个物体的终结者复活。

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

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

终结器必须处理所有这些可能的对象状态,更糟糕的是,由终结器引起的不可用对象状态。正如您所认识到的那样,对象图可以作为一个整体收集,并且所有终结器都可以按任意顺序执行,甚至可以同时执行。所以它不需要循环,也不需要复活尝试来解决问题。当对象A具有对对象B的引用并且两者都具有终结器时,在进程中需要B时,清除A的尝试可能失败,因为B可能已经完成,或者甚至在并发完成的中间。

简而言之,最终确定甚至不适合它最初的清理。这就是为什么finalize()方法已被Java 9弃用了。

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

总而言之,我的根本问题是“给定一个当前正在最终确定的对象,安全地创建它的副本,而不会意外地复活可能在其过程中的参考周期中的任何对象”。

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

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


0
投票

正如其他答案所表明的那样,尝试以这种方式解决潜在问题是无法实现的,并且在尝试解决此类问题时需要进行更广泛的重新思考。这篇文章描述了我习惯于我的问题的解决方案,以及我如何到达那里。

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

允许对象变为未引用然后从终结器中恢复它显然是一个坏主意。然而,“恢复”没有终结器的对象不是一个问题(因为这相当于永远不会被释放的对象 - 它不会像使用终结器的对象那样“部分完成”)。这可以通过使用终结器创建一个单独的对象来实现,并有意在原始对象和单独的终结器承载对象之间创建一个引用循环(它只有一个终结器和对原始对象的引用t,没有别的);当对象变为未引用时,新对象上的终结器将运行,但原始对象将不会被释放,并且不会以任何难以处理的终结相关状态结束。

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

总结:没有安全的方法来保持对象在自己的最终化期间保持活着,即使你将其所有字段复制到其他地方也是如此:相反,你需要确保对象没有终结器,而是使用其他对象来保持对象的活着状态。定稿。

© www.soinside.com 2019 - 2024. All rights reserved.