Вопрос-ответ

finalize() called on strongly reachable objects in Java 8

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, которая имеет следующее:

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

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

Исключение возникает в цепочке вызовов MIMEWriter.writePart, которая выглядит следующим образом:


  1. MIMEWriter.writePart() записывает заголовки для части, затем вызывает part.writeBodyPartContent( this )

  2. MIMEBodyPart.writeBodyPartContent() вызывает наш служебный метод IOUtil.copy( getContentStream(), out ) для потоковой передачи содержимого на вывод

  3. MIMEBodyPart.getContentStream() просто возвращает входной поток, переданный в конструктор (см. Блок кода выше)

  4. IOUtil.copy имеет цикл, который считывает фрагмент размером 8 КБ из входного потока и записывает его в выходной поток до тех пор, пока входной поток не опустеет.

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

Здесь немного предположения. Объект может быть завершен и собран мусор, даже если есть ссылки на него в локальных переменных в стеке, и даже если есть активный вызов метода экземпляра этого объекта в стеке! Требование заключается в том, чтобы объект был недоступен. Даже если он находится в стеке, если никакой последующий код не затрагивает эту ссылку, он потенциально недоступен.

Смотрите Этот другой ответ для примера того, как объект может быть обработан, пока локальная переменная, ссылающаяся на него, все еще находится в области видимости.

Вот пример того, как объект может быть завершен, пока активен вызов метода экземпляра:

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

Обновить

В комментариях 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();
}

снова происходит завершение. Я подозреваю, что причина в том, что без цикла main() метод интерпретируется, а не компилируется. Интерпретатор, вероятно, менее агрессивен в отношении анализа достижимости. При выполнении цикла yield main() метод компилируется, и JIT-компилятор обнаруживает, что finalizeThis стал недоступен во время выполнения loop() метода.

Другим способом запуска такого поведения является использование -Xcomp опции JVM, которая заставляет методы компилироваться JIT перед выполнением. Я бы не стал запускать все приложение таким образом - JIT-компиляция всего может быть довольно медленной и занимать много места - но это полезно для устранения подобных случаев в небольших тестовых программах, вместо того, чтобы возиться с циклами.

Ответ 2

finalize имеет 99 проблем, и преждевременная доработка является новой.

В Java 9 появилась Reference.reachabilityFence для решения этой проблемы. В документации также упоминается использование synchronized (obj) { ... } в качестве альтернативы в Java 8.

Но реальное решение - не использовать finalize.

Ответ 3

Ваш финализатор неверен.

Во-первых, ему не нужен блок catch, и он должен вызываться super.finalize() в своем собственном finally{} блоке. Каноническая форма финализатора выглядит следующим образом:

protected void finalize() throws Throwable
{
try
{
// do stuff
}
finally
{
super.finalize();
}
}

Во-вторых, вы предполагаете, что у вас есть единственная ссылка на m_stream, которая может быть правильной, а может и нет. Элемент m_stream должен завершить работу сам. Но для этого вам ничего не нужно делать. В конечном итоге m_stream будет FileInputStream or FileOutputStream или поток сокетов, и они уже завершаются корректно.

Я бы просто удалил это.

java java-8