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
, который заключается в следующем:
MIMEWriter.writePart()
пишет заголовки для части, затем вызываетpart.writeBodyPartContent( this )
MIMEBodyPart.writeBodyPartContent()
вызывает наш метод утилитыIOUtil.copy( getContentStream(), out )
для потоковой передачи содержимого на выходMIMEBodyPart.getContentStream()
просто возвращает входной поток, переданный в contstructor (см. блок кода выше)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()
методы. При этом, кто-нибудь сталкивался с этой проблемой раньше? У кого-нибудь есть идеи относительно причины?
3 ответа
Немного догадок здесь. Возможно завершение объекта и сбор мусора, даже если в стеке есть ссылки на него в локальных переменных, и даже если в стеке есть активный вызов метода экземпляра этого объекта! Требуется, чтобы объект был недоступен. Даже если он находится в стеке, если последующий код не касается этой ссылки, он потенциально недоступен.
Посмотрите этот другой ответ для примера того, как объект может быть 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'ed. На JDK 8 GA это печатает следующее:
loop() called
finalized!
loop() returns
каждый раз.
Нечто подобное может происходить с MimeBodyPart
, Хранится ли он в локальной переменной? (Кажется, так как код, кажется, придерживается соглашения, что поля именуются с m_
префикс.)
ОБНОВИТЬ
В комментариях ФП предложил внести следующие изменения:
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-компиляция может быть довольно медленной и занимать много места - но это полезно для удаления подобных случаев в маленьких тестовых программах вместо того, чтобы возиться с циклами.
имеет 99 проблем, и преждевременная доработка - новая.
Java 9 представила Reference.reachabilityFence для решения этой проблемы. В документации также упоминается использование
synchronized (obj) { ... }
в качестве альтернативы на Java 8.
Но настоящее решение - не использовать
finalize
.
Ваш финализатор не верен.
Во-первых, ему не нужен блок catch, и он должен вызывать super.finalize()
в своем собственном finally{}
блок. Каноническая форма финализатора выглядит следующим образом:
protected void finalize() throws Throwable
{
try
{
// do stuff
}
finally
{
super.finalize();
}
}
Во-вторых, вы предполагаете, что держите единственную ссылку на m_stream
, который может быть или не быть правильным. m_stream
Участник должен завершить сам. Но вам не нужно ничего делать, чтобы достичь этого. в конечном счете m_stream
будет FileInputStream
или же FileOutputStream
или поток сокетов, и они уже завершают себя правильно.
Я бы просто удалил это.