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
extends HTTPMessage
, который имеет следующее:
public void close() throws IOException
{
if ( m_stream != null )
{
m_stream.close();
}
}
protected void finalize()
{
try
{
close();
}
catch ( final Exception ignored ) { }
}
Исключение происходит в цепочке вызовов MIMEWriter.writePart
, которая выглядит следующим образом:
-
MIMEWriter.writePart()
записывает заголовки для части, затем вызывает part.writeBodyPartContent( this )
-
MIMEBodyPart.writeBodyPartContent()
вызывает наш служебный метод IOUtil.copy( getContentStream(), out )
для потоковой передачи содержимого на вывод
-
MIMEBodyPart.getContentStream()
просто возвращает входной поток, переданный в контрструктор (см. блок кода выше)
-
IOUtil.copy
имеет цикл, который считывает блок 8K из входного потока и записывает его в выходной поток до тех пор, пока входной поток не станет пустым.
Вызывается 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)
Мы помещаем некоторый журнал в метод HTTPMessage.close()
, который регистрировал трассировку стека вызывающего и доказал, что это определенно поток финализатора, вызывающий HTTPMessage.finalize()
, пока выполняется IOUtil.copy()
.
Объект MIMEBodyPart
определенно доступен из текущего стека потоков как this
в фрейме стека для MIMEBodyPart.writeBodyPartContent
. Я не понимаю, почему JVM будет называть finalize()
.
Я попытался извлечь соответствующий код и запустить его в узком цикле на моей собственной машине, но я не могу воспроизвести проблему. Мы можем надежно воспроизвести проблему с высокой нагрузкой на одном из наших серверов-разработчиков, но все попытки создать меньший воспроизводимый тестовый пример потерпели неудачу. Код компилируется под Java 7, но выполняется под Java 8. Если мы перейдем на Java 7 без перекомпиляции, проблема не возникает.
В качестве обходного пути я переписал затронутый код с помощью библиотеки электронной почты Java Mail MIME, и проблема исчезла (предположительно, Java Mail не использует finalize()
). Тем не менее, я обеспокоен тем, что другие методы finalize()
в приложении могут быть вызваны некорректно или что Java пытается уничтожить все объекты, которые все еще используются.
Я знаю, что действующая передовая практика рекомендует использовать finalize()
, и я, вероятно, вернусь к этой домашней библиотеке, чтобы удалить методы finalize()
. Говоря это, кто-нибудь сталкивался с этим вопросом раньше? Кто-нибудь имеет какие-либо идеи относительно причины?
Ответы
Ответ 1
Немного гипотезы здесь. Возможно, что объект будет финализирован и собран мусором, даже если в нем есть ссылки на него в локальных переменных в стеке, и даже если есть активный вызов метода экземпляра этого объекта в стеке! Требование состоит в том, чтобы объект был недоступен. Даже если он находится в стеке, если последующий код не касается этой ссылки, он потенциально недоступен.
См. этот другой ответ для примера того, как объект может быть 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) {
new FinalizeThis().loop();
}
}
Пока метод loop()
активен, нет никакой возможности какого-либо кода делать что-либо со ссылкой на объект FinalizeThis
, поэтому он недоступен. И поэтому он может быть доработан и GC'ed. В JDK 8 GA это печатает следующее:
loop() called
finalized!
loop() returns
каждый раз.
Что-то подобное может происходить с MimeBodyPart
. Сохраняется ли она в локальной переменной? (Кажется, так, поскольку код, похоже, придерживается соглашения о том, что поля называются префиксом m_
.)
UPDATE
В комментариях ОП предложила внести следующие изменения:
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()
интерпретируется, а не компилируется. Интерпретатор, вероятно, менее агрессивен в анализе достижимости. С использованием цикла доходности метод main()
компилируется, и компилятор JIT обнаруживает, что FinalizeThis
стал недоступным, когда выполняется метод loop()
.
Другим способом запуска этого поведения является использование опции -Xcomp
для JVM, которая заставляет методы скомпилировать JIT перед выполнением. Я бы не запускал все приложение таким образом - JIT-компиляция все может быть довольно медленной и занимать много места - но это полезно для очистки таких случаев в небольших тестовых программах, вместо того, чтобы перебирать петли.
Ответ 2
Ваш финализатор неправильный.
Во-первых, ему не нужен блок catch, и он должен вызывать super.finalize()
в своем собственном блоке finally{}
. Каноническая форма финализатора такова:
protected void finalize() throws Throwable
{
try
{
// do stuff
}
finally
{
super.finalize();
}
}
Во-вторых, вы предполагаете, что у вас есть единственная ссылка на m_stream
, которая может быть или не быть правильной. Член m_stream
должен финализировать себя. Но для этого вам не нужно ничего делать. В конечном итоге m_stream
будет FileInputStream
или FileOutputStream
или потоком сокета, и они уже правильно финализируются.
Я просто удалю его.