finalize()在Java 8中调用强可达对象

Nat*_*han 22 java garbage-collection finalize finalizer java-8

我们最近将我们的消息处理应用程序从Java 7升级到Java 8.自升级以来,我们偶尔会遇到一个流在读取时被关闭的异常.记录显示终结器线程正在调用finalize()保存流的对象(进而关闭流).

代码的基本概要如下:

MIMEWriter writer = new MIMEWriter( out );
in = new InflaterInputStream( databaseBlobInputStream );
MIMEBodyPart attachmentPart = new MIMEBodyPart( in );
writer.writePart( attachmentPart );
Run Code Online (Sandbox Code Playgroud)

MIMEWriter并且MIMEBodyPart是本土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 ) { }
}
Run Code Online (Sandbox Code Playgroud)

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

  1. MIMEWriter.writePart() 写入部件的标题,然后调用 part.writeBodyPartContent( this )
  2. MIMEBodyPart.writeBodyPartContent()调用我们的实用工具方法IOUtil.copy( getContentStream(), out )将内容流式传输到输出
  3. MIMEBodyPart.getContentStream() 只返回传递给contstructor的输入流(参见上面的代码块)
  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)
Run Code Online (Sandbox Code Playgroud)

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

MIMEBodyPart对象绝对可以从当前线程的堆栈中到达,如this在堆栈帧中MIMEBodyPart.writeBodyPartContent.我不明白为什么JVM会调用finalize().

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

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

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

Stu*_*rks 27

这里有点猜想.即使在堆栈上的局部变量中有对象的引用,也可以最终确定对象并进行垃圾收集,即使堆栈上存在对该对象的实例方法的活动调用!要求是对象无法访问.即使它在堆栈上,如果没有后续代码接触该引用,它也可能无法访问.

有关如何在引用它的局部变量仍在范围内时对象如何进行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();
    }
}
Run Code Online (Sandbox Code Playgroud)

当该loop()方法处于活动状态时,任何代码都不可能通过对FinalizeThis对象的引用执行任何操作,因此它无法访问.因此,它可以最终确定和GC.在JDK 8 GA上,这将打印以下内容:

loop() called
finalized!
loop() returns
Run Code Online (Sandbox Code Playgroud)

每次.

可能会发生类似的事情MimeBodyPart.它存储在局部变量中吗?(似乎是这样,因为代码似乎遵循一个约定,即使用m_前缀命名字段.)

UPDATE

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

    public static void main(String[] args) {
        FinalizeThis finalizeThis = new FinalizeThis();
        finalizeThis.loop();
    }
Run Code Online (Sandbox Code Playgroud)

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

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

终止再次发生.我怀疑原因是没有循环,main()方法被解释,而不是编译.解释器对可达性分析的攻击性可能较低.使用yield循环后,该main()方法将被编译,并且JIT编译器检测到finalizeThisloop()方法在执行时已无法访问.

触发此行为的另一种方法是使用-XcompJVM选项,该选项强制在执行之前对方法进行JIT编译.我不会以这种方式运行整个应用程序 - JIT编译一切都很慢并占用大量空间 - 但它对于在小测试程序中清除这样的情况很有用,而不是修补循环.

  • 另一句话:如果JVM确定"没有后续代码接触该引用"并最终确定它,那么这是一个明确的信号,即调用者忘记调用`close()`方法,不是吗?毕竟,调用`close()`方法意味着触摸引用,所以finalize方法实现了它的目的:关闭忘记关闭的东西(只是有点太早)... (5认同)
  • @Holger是的,让终结者发挥可以等待的副作用是一种历史悠久的技术.关闭流,OP的应用程序显然有一些调用代码创建流,然后传递给`MIMEBodyPart`构造函数并存储在一个字段中.它是`MIMEBodyPart`实例变得无法访问并最终完成,它会关闭流.调用代码仍然具有对流的引用,并在尝试使用它时将其关闭.结论是`MIMEBodyPart`终结器不应该关闭流,因为它没有打开它. (4认同)
  • 谢谢@Stuart Marks - 你已经恢复了我对Stack Overflow的信心.关于你的程序的有趣想法是它在Java 7和Java 8上给我相同.我的问题只在我们切换到Java 8时出现.你的分析虽然有意义,但进一步激励我从我们的`finalize()`中删除代码库. (3认同)
  • @Stuart Marks:类`MIMEBodyPart`有一个`finalize()`**和**一个`close()`方法,如问题所示.`finalize()`不直接在流上调用`close()`,而是通过它自己的实例方法`close()`,然后在流上调用`close()`.因此,似乎目的是在`MIMEBodyPart`实例上调用`close()`,如果没有调用`close()`,则`finalize()`帮助.这似乎就是这种情况,`MIMEBodyPart`实例的`close()`方法没有被调用,否则它不会在调用之前被垃圾收集. (3认同)
  • @Nathan我已经更新了我的回答以回应你的评论.不确定为什么你只在Java 8上的应用程序中看到问题.一堆JIT启发式可能在7到8之间发生了变化,这可能导致像这样的行为差异.此外,如果删除终结器,请确保最终关闭流.也许使用try-with-resources. (2认同)
  • 由于终结可以由任何线程执行,理论上输出可能是竞争条件的结果,而循环和终结之间没有任何时间关系.我建议让`finalize`方法修改一个`static volatile boolean`变量和循环读取该变量,这样它甚至可以让循环本身在运行时检测到它自己的`this`终结(并且它可能会从`检测完成时循环,所以我们不必等待程序终止超过必要的). (2认同)