finalize()在Java 8中调用强可到达的对象
我们最近将我们的消息处理应用程序从Java 7升级到了Java 8.自从升级之后,我们偶然发现一个stream正在被读取时closures的exception。 日志logging显示终结器线程正在调用持有stream的对象的finalize()
(它依次closuresstream)。
代码的基本概述如下:
MIMEWriter writer = new MIMEWriter( out ); in = new InflaterInputStream( databaseBlobInputStream ); MIMEBodyPart attachmentPart = new MIMEBodyPart( in ); writer.writePart( attachmentPart );
MIMEWriter
和MIMEBodyPart
是本土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
的调用链发生exception,如下所示:
-
MIMEWriter.writePart()
为零件写入标题,然后调用part.writeBodyPartContent( this )
-
MIMEBodyPart.writeBodyPartContent()
调用我们的实用方法IOUtil.copy( getContentStream(), out )
将内容stream式传输到输出 -
MIMEBodyPart.getContentStream()
只是返回传递给MIMEBodyPart.getContentStream()
的inputstream(参见上面的代码块) -
IOUtil.copy
有一个循环,从inputstream中读取一个8K块,并将其写入输出stream,直到inputstream为空。
MIMEBodyPart.finalize()
在IOUtil.copy
运行时被调用,并得到以下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)
我们在logging调用者的堆栈跟踪的HTTPMessage.close()
方法中放入了一些日志logging,并certificate它确实是在IOUtil.copy()
运行时调用HTTPMessage.finalize()
的终结器线程。
在MIMEBodyPart
的堆栈框架中, MIMEBodyPart.writeBodyPartContent
对象肯定可从当前线程的堆栈MIMEBodyPart.writeBodyPartContent
。 我不明白为什么JVM会调用finalize()
。
我尝试提取相关的代码,并在我自己的机器上运行一个紧密的循环,但我不能重现这个问题。 我们可以在我们的一个开发服务器上以高负载可靠地再现问题,但任何尝试创build一个较小的可重复testing用例都失败了。 代码在Java 7下编译,但在Java 8下执行。如果我们切换回Java 7而不重新编译,问题就不会发生。
作为解决方法,我使用Java Mail MIME库重写了受影响的代码,问题已消失(推测Java Mail不使用finalize()
)。 但是,我担心应用程序中的其他finalize()
方法可能会被错误地调用,或者Java试图垃圾收集仍在使用中的对象。
我知道目前的最佳做法build议不要使用finalize()
,我可能会重新访问这个本土库去除finalize()
方法。 话虽如此,有没有人遇到过这个问题? 有没有人有任何想法的原因?
这里有点猜测。 即使在堆栈中的局部variables中引用了对象,并且即使在堆栈上存在该对象的实例方法的活动调用,也可能会终止对象并进行垃圾收集! 要求是对象不可达 。 即使它在堆栈上,如果没有后续的代码触及那个引用,它可能无法访问。
有关如何在引用它的局部variables仍处于作用域内的情况下可以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(); } }
当loop()
方法处于活动状态时,任何代码都不可能对FinalizeThis
对象的引用做任何处理,因此无法访问。 因此它可以最终确定和GC。 在JDK 8 GA上,这将打印以下内容:
loop() called finalized! loop() returns
每次。
MimeBodyPart
可能会出现类似的情况。 它被存储在一个局部variables? (似乎是这样,因为代码似乎遵循一个约定,即以m_
前缀命名字段。)
UPDATE
在评论中,OPbuild议做出如下修改:
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(); }
定案再次发生。 我怀疑原因是没有循环, main()
方法被解释,而不是编译。 解释者对可达性分析可能不那么积极。 使用yield循环, main()
方法被编译,并且JIT编译器检测到finalizeThis
在loop()
方法执行时变得无法访问。
触发此行为的另一种方法是对JVM使用-Xcomp
选项,强制在执行之前对方法进行JIT编译。 我不会以这种方式运行整个应用程序–JIT编译一切可能会很慢并且占用大量空间 – 但是在小testing程序中清除这种情况非常有用,而不是修补循环。
你的终结者是不正确的。
首先,它不需要catch块,它必须在它自己的finally{}
块中调用super.finalize()
。 终结者的规范forms如下:
protected void finalize() throws Throwable { try { // do stuff } finally { super.finalize(); } }
其次,你假设你持有对m_stream
的唯一引用,这可能是也可能不是正确的。 m_stream
成员应该自行完成。 但是你不需要做任何事情来完成。 最终m_stream
将是一个FileInputStream
或FileOutputStream
或一个套接字stream,他们已经正确完成自己。
我只是删除它。