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

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

我们最近将我们的消息处理应用程序从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. qazxsw poi只返回传递给构造函数的输入流(参见上面的代码块)
  4. MIMEBodyPart.getContentStream()有一个循环,它从输入流中读取一个8K块,并将其写入输出流,直到输入流为空。

IOUtil.copy运行时调用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()对象绝对可以从当前线程的堆栈中作为MIMEBodyPart的堆栈帧中的this到达。我不明白为什么JVM会调用MIMEBodyPart.writeBodyPartContent

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

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

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

java garbage-collection java-8 finalizer finalize
2个回答
26
投票

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

有关如何在引用它的局部变量仍在范围内时对象如何进行GC的示例,请参阅finalize()

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

this other answer

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()对象执行任何操作,因此它无法访问。因此,它可以最终确定和GC。在JDK 8 GA上,这将打印以下内容:

FinalizeThis

每次。

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

UPDATE

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

m_

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

    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循环的到位,main()方法被编译,并且JIT编译器在main()方法执行时检测到finalizeThis变得无法访问。

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


1
投票

你的终结器不正确。

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

finally{}

其次,你假设你持有protected void finalize() throws Throwable { try { // do stuff } finally { super.finalize(); } } 的唯一参考,这可能是也可能不正确。 m_stream成员应该自己完成。但是你不需要做任何事来完成它。最终m_stream将是m_streamFileInputStream或套接字流,他们已经正确地完成了自己。

我会删除它。

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