finalize() 在 Java 8 中调用强可访问的对象

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

代码的基本大纲如下:

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

MIMEWriter并且是自家开发的MIME / HTTP库的一部分。 扩展 ,它具有以下各项:MIMEBodyPartMIMEBodyPartHTTPMessage

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()只是返回传递到 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)

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

该对象绝对可以从当前线程的堆栈访问,就像 在 的堆栈帧中一样。我不明白为什么JVM会调用.MIMEBodyPartthisMIMEBodyPart.writeBodyPartContentfinalize()

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

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

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


答案 1

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

请参阅此其他答案,了解在引用对象的局部变量仍在作用域中时如何对对象进行 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();
    }
}

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

loop() called
finalized!
loop() returns

每次。

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

更新

在评论中,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();
    }

再次完成。我怀疑原因是没有循环,方法被解释,而不是编译。口译员对可达性分析可能没有那么激进。使用 yield 循环后,将编译该方法,并且 JIT 编译器会在方法执行时检测到该方法已变得无法访问。main()main()finalizeThisloop()

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


答案 2

您的终结器不正确。

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

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

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

我会删除它。