简体   繁体   English

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

[英]finalize() called on strongly reachable objects in Java 8

We recently upgraded our message processing application from Java 7 to Java 8. Since the upgrade, we get an occasional exception that a stream has been closed while it is being read from.我们最近将我们的消息处理应用程序从 Java 7 升级到了 Java 8。自升级以来,我们偶尔会遇到一个异常,即流在读取时已关闭。 Logging shows that the finalizer thread is calling finalize() on the object that holds the stream (which in turn closes the stream).日志记录显示终结器线程正在对保存流的对象调用finalize() (反过来关闭流)。

The basic outline of the code is as follows:代码的基本轮廓如下:

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

MIMEWriter and MIMEBodyPart are part of a home-grown MIME/HTTP library. MIMEWriterMIMEBodyPart是本土 MIME/HTTP 库的一部分。 MIMEBodyPart extends HTTPMessage , which has the following: MIMEBodyPart扩展了HTTPMessage ,它具有以下内容:

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

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

The exception occurs in the invocation chain of MIMEWriter.writePart , which is as follows:异常发生在MIMEWriter.writePart的调用链中,如下:

  1. MIMEWriter.writePart() writes the headers for the part, then calls part.writeBodyPartContent( this ) MIMEWriter.writePart()写入部件的标头,然后调用part.writeBodyPartContent( this )
  2. MIMEBodyPart.writeBodyPartContent() calls our utility method IOUtil.copy( getContentStream(), out ) to stream the content to the output MIMEBodyPart.writeBodyPartContent()调用我们的实用方法IOUtil.copy( getContentStream(), out )将内容流式传输到输出
  3. MIMEBodyPart.getContentStream() just returns the input stream passed into the contstructor (see code block above) MIMEBodyPart.getContentStream()只返回传递给构造函数的输入流(参见上面的代码块)
  4. IOUtil.copy has a loop that reads an 8K chunk from the input stream and writes it to the output stream until the input stream is empty. IOUtil.copy有一个循环,它从输入流中读取一个 8K 的块并将其写入输出流,直到输入流为空。

The MIMEBodyPart.finalize() is called while IOUtil.copy is running, and it gets the following exception: 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)

We put some logging in the HTTPMessage.close() method that logged the stack trace of the caller and proved that it is definitely the finalizer thread that is invoking HTTPMessage.finalize() while IOUtil.copy() is running.我们在HTTPMessage.close()方法中放置了一些日志,该方法记录了调用者的堆栈跟踪,并证明它肯定是在IOUtil.copy()运行时调用HTTPMessage.finalize()的终结器线程。

The MIMEBodyPart object is definitely reachable from the current thread's stack as this in the stack frame for MIMEBodyPart.writeBodyPartContent . MIMEBodyPart对象绝对可以从当前线程的堆栈访问,因为thisMIMEBodyPart.writeBodyPartContent的堆栈帧中。 I don't understand why the JVM would call finalize() .我不明白为什么 JVM 会调用finalize()

I tried extracting the relevant code and running it in a tight loop on my own machine, but I cannot reproduce the problem.我尝试提取相关代码并在我自己的机器上以紧密循环的方式运行它,但我无法重现该问题。 We can reliably reproduce the problem with high load on one of our dev servers, but any attempts to create a smaller reproducible test case have failed.我们可以在其中一台开发服务器上以高负载可靠地重现该问题,但任何创建较小的可重现测试用例的尝试都失败了。 The code is compiled under Java 7 but executes under Java 8. If we switch back to Java 7 without recompiling, the problem does not occur.代码是在Java 7下编译但在Java 8下执行。如果我们切换回Java 7而不重新编译,则不会出现问题。

As a workaround, I've rewritten the affected code using the Java Mail MIME library and the problem has gone away (presumably Java Mail doesn't use finalize() ).作为一种解决方法,我使用 Java Mail MIME 库重写了受影响的代码,问题已经消失(大概 Java Mail 不使用finalize() )。 However, I'm concerned that other finalize() methods in the application may be called incorrectly, or that Java is trying to garbage-collect objects that are still in use.但是,我担心应用程序中的其他finalize()方法可能会被错误调用,或者 Java 正在尝试对仍在使用的对象进行垃圾收集。

I know that current best practice recommends against using finalize() and I will probably revisit this home-grown library to remove the finalize() methods.我知道当前的最佳实践建议不要使用finalize()并且我可能会重新访问这个本土库以删除finalize()方法。 That being said, has anyone come across this issue before?话虽如此,以前有人遇到过这个问题吗? Does anyone have any ideas as to the cause?有没有人对原因有任何想法?

A bit of conjecture here. 这里有点猜想。 It is possible for an object to be finalized and garbage collected even if there are references to it in local variables on the stack, and even if there is an active call to an instance method of that object on the stack! 即使在堆栈上的局部变量中有对象的引用,也可以最终确定对象并进行垃圾收集,即使堆栈上存在对该对象的实例方法的活动调用! The requirement is that the object be unreachable . 要求是对象无法访问 Even if it's on the stack, if no subsequent code touches that reference, it's potentially unreachable. 即使它在堆栈上,如果没有后续代码接触该引用,它也可能无法访问。

See this other answer for an example of how an object can be GC'ed while a local variable referencing it is still in scope. 有关如何在引用它的局部变量仍在范围内时对象如何进行GC的示例,请参阅此其他答案

Here's an example of how an object can be finalized while an instance method call is active: 以下是在实例方法调用处于活动状态时如何完成对象的示例:

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();
    }
}

While the loop() method is active, there is no possibility of any code doing anything with the reference to the FinalizeThis object, so it's unreachable. loop()方法处于活动状态时,任何代码都不可能通过对FinalizeThis对象的引用执行任何操作,因此它无法访问。 And therefore it can be finalized and GC'ed. 因此,它可以最终确定和GC。 On JDK 8 GA, this prints the following: 在JDK 8 GA上,这将打印以下内容:

loop() called
finalized!
loop() returns

every time. 每次。

Something similar might be going on with MimeBodyPart . MimeBodyPart可能会发生类似情况。 Is it being stored in a local variable? 它存储在局部变量中吗? (It seems so, since the code seems to adhere to a convention that fields are named with an m_ prefix.) (似乎是这样,因为代码似乎遵循一个约定,即使用m_前缀命名字段。)

UPDATE UPDATE

In the comments, the OP suggested making the following change: 在评论中,OP建议进行以下更改:

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

With this change he didn't observe finalization, and neither do I. However, if this further change is made: 有了这个改变,他没有观察到最终确定,我也没有。但是,如果做出进一步的改变:

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

finalization once again occurs. 终止再次发生。 I suspect the reason is that without the loop, the main() method is interpreted, not compiled. 我怀疑原因是没有循环, main()方法被解释,而不是编译。 The interpreter is probably less aggressive about reachability analysis. 解释器对可达性分析的攻击性可能较低。 With the yield loop in place, the main() method gets compiled, and the JIT compiler detects that finalizeThis has become unreachable while the loop() method is executing. 使用yield循环, main()方法被编译,并且JIT编译器在loop()方法执行时检测到finalizeThis变得无法访问。

Another way of triggering this behavior is to use the -Xcomp option to the JVM, which forces methods to be JIT-compiled before execution. 触发此行为的另一种方法是对JVM使用-Xcomp选项,这会强制在执行之前对方法进行JIT编译。 I wouldn't run an entire application this way -- JIT-compiling everything can be quite slow and take lots of space -- but it's useful for flushing out cases like this in little test programs, instead of tinkering with loops. 我不会以这种方式运行整个应用程序 - JIT编译一切都很慢并占用大量空间 - 但它对于在小测试程序中清除这样的情况很有用,而不是修补循环。

Your finalizer isn't correct. 你的终结器不正确。

Firstly, it doesn't need the catch block, and it must call super.finalize() in its own finally{} block. 首先,它不需要catch块,它必须在自己的finally{}块中调用super.finalize() The canonical form of a finalizer is as follows: 终结器的规范形式如下:

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

Secondly, you're assuming you are holding the only reference to m_stream , which may or may not be correct. 其次,你假设你持有m_stream的唯一引用,这可能是也可能不正确。 The m_stream member should finalize itself. m_stream成员应该自己完成。 But you don't need to do anything to accomplish that. 但是你不需要做任何事来完成它。 Ultimately m_stream will be a FileInputStream or FileOutputStream or a socket stream, and they already finalize themselves correctly. 最终m_stream将是FileInputStreamFileOutputStream或套接字流,并且它们已经正确地完成了自己。

I would just remove it. 我会删除它。

finalize has 99 problems, and premature finalization is a new one. finalize有 99 个问题,过早的finalize是一个新的问题。

Java 9 has introduced Reference.reachabilityFence to work around this issue. Java 9 引入了Reference.reachabilityFence来解决这个问题。 The documentation also mentions using synchronized (obj) { ... } as an alternative on Java 8.该文档还提到使用synchronized (obj) { ... }作为 Java 8 的替代方案。

But the real solution is to not use finalize .但真正的解决方案是不使用finalize

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM