finalize() 在 Java 8 中调用强可达对象 [英] finalize() called on strongly reachable objects in Java 8

查看:29
本文介绍了finalize() 在 Java 8 中调用强可达对象的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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. Logging shows that the finalizer thread is calling finalize() on the object that holds the stream (which in turn closes the stream).

代码的基本大纲如下:

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

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

MIMEWriter and MIMEBodyPart are part of a home-grown MIME/HTTP library. MIMEBodyPart extends HTTPMessage, which has the following:

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

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

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

The exception occurs in the invocation chain of MIMEWriter.writePart, which is as follows:

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

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

The MIMEBodyPart.finalize() is called while IOUtil.copy is running, and it gets the following exception:

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() 正在运行.

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.

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

The MIMEBodyPart object is definitely reachable from the current thread's stack as this in the stack frame for MIMEBodyPart.writeBodyPartContent. I don't understand why the JVM would call finalize().

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

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 Mail MIME 库重写了受影响的代码,问题消失了(大概 Java Mail 不使用 finalize()).但是,我担心应用程序中的其他 finalize() 方法可能会被错误调用,或者 Java 正在尝试对仍在使用的对象进行垃圾收集.

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()). 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(),我可能会重新访问这个自制的库以删除 finalize() 方法.话虽如此,以前有人遇到过这个问题吗?有没有人对原因有任何想法?

I know that current best practice recommends against using finalize() and I will probably revisit this home-grown library to remove the finalize() methods. 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.

请参阅其他答案,了解如何在局部变量引用对象时对对象进行 GC 的示例仍在范围内.

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.

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

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

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

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. And therefore it can be finalized and GC'ed. On JDK 8 GA, this prints the following:

loop() called
finalized!
loop() returns

每次.

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

Something similar might be going on with 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.)

更新

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

In the comments, the OP suggested making the following change:

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

再次发生终结.我怀疑原因是没有循环, main() 方法被解释,而不是编译.解释器对可达性分析可能不太积极.有了 yield 循环,main() 方法被编译,JIT 编译器检测到 finalizeThis 变得不可访问,而 loop()> 方法正在执行.

finalization once again occurs. I suspect the reason is that without the loop, the main() method is interpreted, not compiled. 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.

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

Another way of triggering this behavior is to use the -Xcomp option to the JVM, which forces methods to be JIT-compiled before execution. 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.

这篇关于finalize() 在 Java 8 中调用强可达对象的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆