【问题标题】:finalize() called on strongly reachable objects in Java 8finalize() 在 Java 8 中调用强可达对象
【发布时间】:2014-12-25 20:29:48
【问题描述】:

我们最近将消息处理应用程序从 Java 7 升级到了 Java 8。升级后,我们偶尔会遇到异常,即在读取流时已关闭它。日志显示终结器线程正在对持有流的对象调用finalize()(这反过来又关闭了流)。

代码基本大纲如下:

MIMEWriter writer = new MIMEWriter( out );
in = new InflaterInputStream( databaseBlobInputStream );
MIMEBodyPart attachmentPart = new MIMEBodyPart( in );
writer.writePart( attachmentPart );

MIMEWriterMIMEBodyPart 是本地 MIME/HTTP 库的一部分。 MIMEBodyPart 扩展 HTTPMessage,具有以下内容:

public void close() throws IOException
{
    if ( m_stream != null )
    {
        m_stream.close();
    }
}

protected void finalize()
{
    try
    {
        close();
    }
    catch ( final Exception ignored ) { }
}

异常发生在MIMEWriter.writePart的调用链中,如下:

  1. MIMEWriter.writePart() 写入部分的标题,然后调用 part.writeBodyPartContent( this )
  2. MIMEBodyPart.writeBodyPartContent() 调用我们的实用方法 IOUtil.copy( getContentStream(), out ) 将内容流式传输到输出
  3. MIMEBodyPart.getContentStream() 只是返回传入构造函数的输入流(见上面的代码块)
  4. IOUtil.copy 有一个循环,从输入流中读取一个 8K 块并将其写入输出流,直到输入流为空。

MIMEBodyPart.finalize()IOUtil.copy 运行时被调用,它得到以下异常:

java.io.IOException: Stream closed
    at java.util.zip.InflaterInputStream.ensureOpen(InflaterInputStream.java:67)
    at java.util.zip.InflaterInputStream.read(InflaterInputStream.java:142)
    at java.io.FilterInputStream.read(FilterInputStream.java:107)
    at com.blah.util.IOUtil.copy(IOUtil.java:153)
    at com.blah.core.net.MIMEBodyPart.writeBodyPartContent(MIMEBodyPart.java:75)
    at com.blah.core.net.MIMEWriter.writePart(MIMEWriter.java:65)

我们在 HTTPMessage.close() 方法中添加了一些日志记录,该方法记录了调用者的堆栈跟踪,并证明在 IOUtil.copy() 运行时调用 HTTPMessage.finalize() 的肯定是终结器线程。

MIMEBodyPart 对象绝对可以从当前线程的堆栈中以thisMIMEBodyPart.writeBodyPartContent 的堆栈帧中访问。我不明白为什么JVM会调用finalize()

我尝试提取相关代码并在我自己的机器上紧密循环运行它,但我无法重现该问题。我们可以在我们的一个开发服务器上以高负载可靠地重现问题,但是任何创建更小的可重现测试用例的尝试都失败了。代码在Java 7下编译,但在Java 8下执行。如果我们不重新编译就切换回Java 7,就不会出现问题。

作为一种解决方法,我已经使用 Java Mail MIME 库重写了受影响的代码,并且问题已经消失(可能 Java Mail 不使用finalize())。但是,我担心应用程序中的其他 finalize() 方法可能被错误地调用,或者 Java 正在尝试对仍在使用的对象进行垃圾收集。

我知道当前的最佳实践建议不要使用finalize(),我可能会重新访问这个本土库以删除finalize() 方法。话虽这么说,以前有人遇到过这个问题吗?有人对原因有任何想法吗?

【问题讨论】:

  • 如果没有其他解释,我会感到惊讶。当前线程始终是收集器识别活动对象的“根”。您如何确定在您的 IOUtils.copy() 返回之前 调用了终结器?
  • 这听起来很像 JIT 错误。我会在打开 JIT 调试的情况下运行,看看那里是否有任何模式。
  • @Bhaskar,异常证明IOUtil.copy()正在执行时流已关闭。
  • @chrylis,您建议打开哪些标志?是否只是-XX:+PrintCompilation 尝试查看问题的发生是否与其中一种方法的 JIT 编译一致?
  • 是否有第二个无法访问的MIMEBodyPart 持有对同一个 m_stream 对象的引用?

标签: java garbage-collection java-8 finalizer finalize


【解决方案1】:

您的终结器不正确。

首先,它不需要catch块,它必须在自己的finally{}块中调用super.finalize()。终结器的规范形式如下:

protected void finalize() throws Throwable
{
    try
    {
        // do stuff
    }
    finally
    {
        super.finalize();
    }
}

其次,您假设您持有对m_stream 的唯一引用,这可能正确也可能不正确。 m_stream 成员应自行完成。但是你不需要做任何事情来完成它。最终m_stream 将是FileInputStreamFileOutputStream 或套接字流,它们已经正确地完成了自己。

我会删除它。

【讨论】:

  • 我原则上同意调用super.finalize(),但在这种情况下HTTPMessage 扩展了Object,它有一个空的finalize()。我同意目前的代码不是最佳的,但我也不确定这是否相关。问题是 finalize() 在 Java 8 下似乎被错误地调用,但它不是在 Java 7 下。
  • 我还是会删除它,看看会发生什么。您不需要它,由于我所说的所有原因,它可能是错误的来源。
  • 我同意在这种情况下不需要finalize()(我们在代码的更下方直接调用in.close())。但是,我们的 HTTP/MIME 库用于可能需要finalize() 的其他场景。正如我所提到的,我将在将来重新访问该库及其所有用途以删除 finalize()。我也已经重写了有问题的代码以避免这个问题。我们的代码的其他区域也有finalize() 方法。我担心我们可能会在代码的其他部分遇到同样的问题。
  • 可以先调用super.finalize(),还是只能在这个终结器需要做的任何事情之后调用它?
  • @DavidConrad 你可能很幸运,但为什么呢?资源应该按照分配的相反顺序释放。
【解决方案2】:

这里有点猜想。即使堆栈上的局部变量中有对它的引用,并且即使在堆!要求是对象不可访问。即使它在堆栈上,如果没有后续代码触及该引用,它也可能无法访问。

请参阅this other answer 了解如何在引用它的局部变量仍在范围内时对对象进行 GC 的示例。

这是一个在实例方法调用处于活动状态时如何完成对象的示例:

class FinalizeThis {
    protected void finalize() {
        System.out.println("finalized!");
    }

    void loop() {
        System.out.println("loop() called");
        for (int i = 0; i < 1_000_000_000; i++) {
            if (i % 1_000_000 == 0)
                System.gc();
        }
        System.out.println("loop() returns");
    }

    public static void main(String[] args) {
        new FinalizeThis().loop();
    }
}

虽然loop() 方法处于活动状态,但任何代码都不可能对FinalizeThis 对象的引用做任何事情,因此无法访问。因此它可以被最终确定和 GC'ed。在 JDK 8 GA 上,这会打印以下内容:

loop() called
finalized!
loop() returns

每次。

MimeBodyPart 可能会发生类似的事情。它是否存储在局部变量中? (看起来是这样,因为代码似乎遵守了字段以m_ 前缀命名的约定。)

更新

在 cmets 中,OP 建议进行以下更改:

    public static void main(String[] args) {
        FinalizeThis finalizeThis = new FinalizeThis();
        finalizeThis.loop();
    }

有了这个改变,他没有观察到最终确定,我也没有。但是,如果进行进一步的改变:

    public static void main(String[] args) {
        FinalizeThis finalizeThis = new FinalizeThis();
        for (int i = 0; i < 1_000_000; i++)
            Thread.yield();
        finalizeThis.loop();
    }

最终确定再次发生。我怀疑原因是没有循环,main() 方法被解释,而不是编译。解释器可能对可达性分析不那么激进。使用 yield 循环,main() 方法被编译,JIT 编译器检测到finalizeThisloop() 方法执行时变得不可访问。

触发此行为的另一种方法是使用 JVM 的 -Xcomp 选项,这会强制方法在执行前进行 JIT 编译。我不会以这种方式运行整个应用程序 - JIT 编译所有内容可能会非常慢并且占用大量空间 - 但它对于在小测试程序中清除此类案例很有用,而不是修补循环。

【讨论】:

  • 感谢@Stuart Marks - 你恢复了我对 Stack Overflow 的信心。关于你的程序的有趣想法是它在 Java 7 和 Java 8 上给了我相同的结果。我的问题只在我们切换到 Java 8 时出现。不过你的分析是有道理的,并进一步激励我从我们的代码库中删除 finalize() .
  • @Nathan 我已根据您的评论更新了我的答案。不知道为什么您只在 Java 8 上的应用程序中看到问题。一堆 JIT 启发式可能在 7 和 8 之间发生了变化,这可能是造成此类行为差异的原因。此外,如果您删除了终结器,请确保某些东西最终会关闭流。也许使用 try-with-resources。
  • 另外说明:如果 JVM 确定“没有后续代码触及该引用”并完成它,那么很明显表明调用者忘记调用 close() 方法,不是吗?毕竟,调用close() 方法意味着触及引用,因此finalize 方法实现了它的目的:关闭忘记关闭的东西(只是有点太早了)......
  • @Holger 是的,让终结器执行可以等待的副作用是一项历史悠久的技术。重新关闭流,OP 的应用程序显然有一些调用代码创建流,然后将其传递给 MIMEBodyPart 构造函数并存储在一个字段中。这是MIMEBodyPart 实例变得无法访问并最终确定,它关闭了流。调用代码仍然具有对流的引用,并在尝试使用它时发现它已关闭。结论是 MIMEBodyPart 终结器不应该关闭流,因为它没有打开它。
  • @Stuart Marks:MIMEBodyPart 类有一个finalize() 一个close() 方法,如问题中所示。而finalize() 不会直接在流上调用close(),而是通过它自己的实例方法close(),然后在流上调用close()。所以似乎目的是在MIMEBodyPart 实例上调用close(),如果close() 没有被调用,finalize() 会提供帮助。这里似乎就是这种情况,MIMEBodyPart 实例的close() 方法没有被调用,否则它在调用之前无法成为垃圾回收。
【解决方案3】:

finalize有99个问题,过早终结是一个新问题。

Java 9 引入了Reference.reachabilityFence 来解决这个问题。该文档还提到在 Java 8 上使用 synchronized (obj) { ... } 作为替代方案。

但真正的解决方案是不使用finalize

【讨论】:

    猜你喜欢
    • 2011-03-09
    • 1970-01-01
    • 2017-09-19
    • 1970-01-01
    • 2011-10-16
    • 1970-01-01
    • 1970-01-01
    • 2018-12-03
    • 2023-03-12
    相关资源
    最近更新 更多