Pull to refresh

Comments 19

В целом, неплохо, полезная памятка.

Только два коментария:
Лучше освобождать ресурсы программно, а в методе finalize логировать, если этого почему-то сделано не было, чтобы вовремя найти и починить возникшую проблему.

Если использовать собственный логгер, есть вероятность что он уже будет финализирован в момент финализации нашего класса. Например, так может быть при использовании System.runFinalizersOnExit() если последняя строка программы закрывает логгер.

7. Любые исключения выброшенные в теле метода будут проигнорированы.

Более того, объект зависнет в памяти навсегда:
Any exception thrown by the finalize method causes the finalization of this object to be halted


Насчет «зависнет навсегда» — там написано что финализация будет прервана. Насчет сборки ничего не написано. Было бы интересно посмотреть на тест-кейс, где воспроизводится «зависание навсегда»
Действительно, спецификация не утверждает, что сборки не будет, но на практике память не освобождается и рано или поздно нас ждет ООМ. У меня так было в одном проекте и больше всего поразило, что исключение втихую игноририруется. Конечно, может все и от рантайма зависит и тот же OpenJDK этим не страдает.

Вот нашелся эксперимент одного энтузиаста:
elliottback.com/wp/java-memory-leaks-w-finalize-examples/
Думаю, при желании можно и другие найти.
Если вы внимательно прочитаете комментарии к той статье, вы найдете там разоблачение черной магии: нет никаких утечек, просто выполнение чего-то, достаточно тяжелого в finalize() задерживает очередь финализации, и GC просто не успевает угнаться за аллокацией в быстром цикле. Если основной цикл аллокации чуть затормозить — все освобождается.
Действительно, вчитался в коментарии и там говорится, что эксепшен срабатывает как yeild, а потому все может таки рано или поздно стать на круги своя. В моей ситуации до этого не доходило, OOM наступал раньше. Ну и важный нюанс в том, что повторно финализация этого класса выполняться не будет, потому эксепшен в начале метода все-равно даст нам утечку, если что-то критичное не успели освободить.
Но это в любом случае интересное поведение, спасибо вам, что указали на него. Будет интересно посмотреть, воспроизводится ли это с разными видами GC
Если у вашего логера есть метод finalize, то нужно исопльзовать защитную технику: в логгере должен быть волатайл флаг, который выставляется при его финализации, а все метод логирования сначала проверяют этот флаг.

И про второе дополнение тоже спасибо.
Кто-нибудь знает, зачем в конце обязательно вызывать super.finalize() и почему в конце, а не в начале?
<неграмотный плюсист в треде/> Предположу, что finalize — самый что ни на есть обычный метод, а не прямой аналог деструктора, то бишь он перекроет родительский метод при объявлении. Значит надо его явно вызвать в конце, и не в начале, т.к. надо ж соблюсти порядок, обратный инициализации.
Ну что он реально делает? Говорит GC, что теперь объект можно собирать? И получается, если его не вызвать, то объект никогда и не собереться?
В Java6/7, судя по исходникам, он ничего не делает, но в не Sun'овских или в будущих реализациях там запросто может оказаться, например, обработка мягких и фантомных ссылок (всякие ReferenceQueue). Поэтому вызывать нужно.
Не подскажете, а sun.misc.Cleaner действительно быстрее, чем finalize? Везде написаны эти ужасы про медленное выделение и освобождение объектов, имеющих finalize. Даже у Вас про это 430 раз упоминается. Но разве обработка Cleaner имеет не схожий механизм? Мне казалось, что ограничения самой технологии сборки мусора и нет разницы как реализован финализатор — через finalize или Cleaner. Есть какая-нибудь более-менее достоверная информация на этот счёт?
> Есть какая-нибудь более-менее достоверная информация на этот счёт?
Единственная достоверная информация на счет finalize:
Не пользуйтесь!
Нет на сегодняшний день ни одной причины использовать finalize. Если надо освобождать реальные ресурсы, решайте программно.
В комментариях к коду Cleaner написано, что в отличие от finalize он не делает jni up call. Так что получается, что действительно быстрее.
Эх, на досуге проверю :)
Если честно, я не думаю, что он так уж заметно быстрее. Я думаю, что дело здесь в другом. Оставим за кадром то, что у finalize() семантика корявая до невозможности — будем только про производительность: если вы используете finalize(), то память под самим вашим объектом (которая в куче явы) не будет освобождена, покуда финализация не отработает. Более того — не будет освобождена память подо всеми объектами, достижимыми из вашего объекта (они должны оставаться достижимыми, пока не отработает finalize). Это создает массу неочевидных проблем: например, мне сложно представить, как можно реализовать сбор-мусора-с-финализацией для объектов в молодом поколении, которые собираются копирующим GC. Почти наверняка все объекты с finalize() будут перенесены в old-gen, и будут собираться fullGC (вроде бы даже это где-то явно описывалось, но точно не вспомню).

В то же время с использованием фантомных ссылок у рантайма не будет никаких проблем с освобождением памяти в куче явы — память под самим объектом, и всеми достижимыми только из него объектами можно освободить сразу, как только сам объект станет недостижим. В том числе и сразу в молодом поколении, быстрым копирующим сборщиком. Ведь Runnable в Cleaner-е хранит (==должен хранить — в идеале) ссылки только на _внешние_ ресурсы, на ресурсы за пределами кучи. Они — и только они — и будут ждать пока дойдет дело до обработки содержимого ReferenceQueue. То есть этот механизм действительно гораздо дешевле по нагрузке на систему управления памятью в самой яве.

Ага, спасибо, Руслан. Очень разумно звучит. Особенно понравились твои рассуждения про old-gen, про это я вообще ничего не писал и не задумывался даже об этом.
Ну это мои спекуляции. Сам-то объект будет освобожден — а Runnable, который мы создали, чтобы освободить внешние ресуры — нет. Будет ждать-таки пока его из очереди не вытащат. Так что здесь непростой баланс получается, между размером этого Runnable, и размером исходного объекта+ все достижимые только из него.

В любом случае, у finalize() такая хитровымученная семантика, что ее реализация наверняка дорогого стоит именно из-за вымученности. Более прозрачная логика работы ReferenceQueue скорее всего и реализуется проще и эффективнее
Я уже проверил. Finalize действительно намного медленнее — на два порядка минимум. Причём медленнее, как выделение, так и освобождение. Кроме того требует дополнительной памяти — не менее 44 байт. Более того, он выделяет часть памяти не в основной куче java-машины, из-за чего трудно сказать, сколько именно памяти используется программой, да, и вообще начинаются чудеса с выделением-освобождением.
Sign up to leave a comment.

Articles