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

查看:99
本文介绍了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 MIMEBodyPart 是本土MIME / HTTP库的一部分。 MIMEBodyPart 扩展 HTTPMessage ,其中包含以下内容:

<$ p $ m
$ {
$ {
$ {$ b $ if $ {
$}



保护void finalize()
{
尝试
{
close();

catch(final exception ignored){}
}

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


  1. MIMEWriter.writePart()为零件写入标题,然后调用 part.writeBodyPartContent(this)

  2. MIMEBodyPart.writeBodyPartContent()调用我们的实用程序方法 IOUtil.copy(getContentStream(),out) code>将内容流式传输到输出

  3. MIMEBodyPart.getContentStream()仅返回传入contstructor的输入流(请参阅上面的代码块)
  4. IOUtil.copy 有一个循环,它从输入流中读取一个8K块并将其写入输出流,直到输入流为空。

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

  java.io.IOException:Stream在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()正在运行。


$ b

MIMEBodyPart MIMEBodyPart.writeBodyPartCont的堆栈框架中,$ c> object肯定可以从当前线程栈中以 this 耳鼻喉科。我不明白为什么JVM会调用 finalize()



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



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

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

解决方案

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



请参阅这个其他答案为一个例子,说明一个对象如何被GC'ed,而引用它的局部变量仍然在范围内。



下面是一个例子,说明如何在实例方法调用处于活动状态时最终确定对象:

  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){
FinalizeThis()。loop(); (


虽然 loop() 方法处于活动状态,任何代码都不可能对 FinalizeThis 对象的引用做任何事情,因此无法访问。因此它可以最终确定和GC。在JDK 8 GA上,这将打印出以下内容:

  loop()被称为
已完成!
loop()每次都会返回



类似的情况可能与 MimeBodyPart 有关。它被存储在本地变量中吗? (似乎是这样,因为代码似乎遵循一个约定,即以 m _ 前缀命名的字段。)



UPDATE



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

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

有了这个改变,他没有观察到定稿,我也没有。 ,如果进一步更改:

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

定稿再次发生。我怀疑原因是没有循环, main()方法被解释,而不是编译。解释者对可达性分析可能不那么积极。使用yield循环,编译 main()方法,并且JIT编译器检测到 finalizeThis 已成为在执行循环()方法时无法访问。



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


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).

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. 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 ) { }
}

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

  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.

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)

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.

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().

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.

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.

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.

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

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

every time.

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.)

UPDATE

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

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.

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天全站免登陆