在Java 8中对强可访问对象调用finalize()
我们最近将消息处理应用程序从Java7升级到Java8。自升级以来,我们偶尔会遇到一个异常,即在读取流时,该流已关闭。日志记录显示终结器线程正在对保存流的对象调用在Java 8中对强可访问对象调用finalize(),java,garbage-collection,java-8,finalizer,finalize,Java,Garbage Collection,Java 8,Finalizer,Finalize,我们最近将消息处理应用程序从Java7升级到Java8。自升级以来,我们偶尔会遇到一个异常,即在读取流时,该流已关闭。日志记录显示终结器线程正在对保存流的对象调用finalize()(这反过来会关闭流) 守则的基本内容如下: MIMEWriter writer = new MIMEWriter( out ); in = new InflaterInputStream( databaseBlobInputStream ); MIMEBodyPart attachmentPart = new MIM
finalize()
(这反过来会关闭流)
守则的基本内容如下:
MIMEWriter writer = new MIMEWriter( out );
in = new InflaterInputStream( databaseBlobInputStream );
MIMEBodyPart attachmentPart = new MIMEBodyPart( in );
writer.writePart( attachmentPart );
protected void finalize() throws Throwable
{
try
{
// do stuff
}
finally
{
super.finalize();
}
}
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 ) { }
}
loop() called
finalized!
loop() returns
异常发生在MIMEWriter.writePart
的调用链中,如下所示:
MIMEWriter writer = new MIMEWriter( out );
in = new InflaterInputStream( databaseBlobInputStream );
MIMEBodyPart attachmentPart = new MIMEBodyPart( in );
writer.writePart( attachmentPart );
protected void finalize() throws Throwable
{
try
{
// do stuff
}
finally
{
super.finalize();
}
}
MIMEWriter.writePart()
写入部件的标题,然后调用part.writeBodyPartContent(this)
MIMEBodyPart.writeBodyPartContent()
调用我们的实用方法IOUtil.copy(getContentStream(),out)
将内容流式传输到输出MIMEBodyPart.getContentStream()
只返回传递到contstructor的输入流(请参见上面的代码块)IOUtil.copy
有一个循环,该循环从输入流读取8K块并将其写入输出流,直到输入流为空IOUtil.copy
时调用MIMEBodyPart.finalize()
,它会得到以下异常:
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()
方法中添加了一些日志记录,该方法记录了调用方的堆栈跟踪,并证明在运行IOUtil.copy()
时调用HTTPMessage.finalize()
的肯定是终结器线程
在MIMEBodyPart.writeBodyPartContent
的堆栈框架中,从当前线程的堆栈中肯定可以访问MIMEBodyPart
对象,即this
。我不明白为什么JVM会调用finalize()
我试图提取相关代码并在自己的机器上以紧密循环的方式运行它,但我无法重现这个问题。我们可以在一个开发服务器上以高负载可靠地重现问题,但是任何创建较小的可重现测试用例的尝试都失败了。代码在Java7下编译,但在Java8下执行。如果我们在不重新编译的情况下切换回Java7,那么问题就不会发生
作为一种解决方法,我使用Java Mail MIME库重写了受影响的代码,问题已经解决了(可能Java Mail不使用finalize()
)。但是,我担心应用程序中的其他finalize()
方法可能被错误调用,或者Java试图对仍在使用的对象进行垃圾收集
我知道当前的最佳实践建议不要使用
finalize()
,我可能会重新访问这个自行开发的库来删除finalize()
方法。也就是说,以前有人遇到过这个问题吗?有人知道原因吗?您的终结器不正确
首先,它不需要catch块,它必须在自己的finally{}
块中调用super.finalize()
。终结器的标准形式如下所示:
MIMEWriter writer = new MIMEWriter( out );
in = new InflaterInputStream( databaseBlobInputStream );
MIMEBodyPart attachmentPart = new MIMEBodyPart( in );
writer.writePart( attachmentPart );
protected void finalize() throws Throwable
{
try
{
// do stuff
}
finally
{
super.finalize();
}
}
其次,您假设您持有对m_stream
的唯一引用,这可能是正确的,也可能是不正确的。m_流
成员应自行完成。但你不需要做任何事情来实现这一点。最终,m_流
将是一个FileInputStream
或FileOutputStream
或套接字流,并且它们已经正确地完成了自己的定稿
我想把它去掉。这里有点猜测。即使堆栈上的局部变量中存在对对象的引用,即使堆栈上存在对该对象的实例方法的活动调用,对象也有可能被最终确定并被垃圾回收!要求是对象不可访问。即使它在堆栈上,如果后续代码没有触及该引用,它也可能无法访问 有关如何在引用对象的局部变量仍在作用域中时对其进行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();
}
}
每一次
MimeBodyPart
也可能发生类似的情况。它是否存储在局部变量中?(看起来是这样的,因为代码似乎遵守了一个惯例,即字段的命名带有m
前缀。)
更新
在评论中,OP建议进行以下更改:
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();
}
publicstaticvoidmain(字符串[]args){
FinalizeThis FinalizeThis=新的FinalizeThis();
对于(int i=0;i<1_000;i++)
螺纹屈服强度();
finalizeThis.loop();
}
最终确定再次发生。我怀疑原因是没有循环,main()
方法是解释的,而不是编译的。解释器在可达性分析方面可能不那么积极。使用yield循环,编译main()
方法,JIT编译器检测到finalizeThis
在执行loop()
方法时变得不可访问
触发此行为的另一种方法是对JVM使用
-Xcomp
选项,这强制方法在执行之前进行JIT编译。我不会以这种方式运行整个应用程序——JIT编译一切都可能非常慢,占用大量空间——但它对于在小测试程序中刷新这样的情况非常有用,而不是修补循环。如果没有其他解释,我会感到惊讶。当前线程始终为空