Пользователь
0,0
рейтинг
17 августа 2012 в 18:58

Разработка → Опасности метода finalize

JAVA*
Во время написания статьи про использование фантомных ссылок, мне потребовалось сослаться на неудобства возникающие при работе с методом finalize. К тому же, считаю, что данный топик будет полезен всем начинающим java разработчикам, а некоторые пункты будет не лишним вспомнить и матерым программистам, ведь использовать метод finalize очень просто, чего не скажешь о поиске последсвий этого. Даже если вы твердо убеждены никогда не использовать метод finalize, это еще не значит, что ваши предыдущие коллеги их не использовали, и вам не надо понимать как они работают.

Итак, поехали от очевидного к менее интуитивному.
  1. Так как метод finalize вызывается при первой сборке мусора следующей за моментом когда ваш объект стал недостижим, то вполне реально, что он не будет вызван вообще, ведь ваше приложение может закончить свою работу так и не дойдя до этой самой сборки мусора. Хотя, конечно, есть один замечательный метод System.runFinalizersOnExit(true), вызвав который на старте программы, метод finalize все таки сработает у уже недостижимых объектах во время корректной остановки приложения.
  2. Спецификация JVM не определяет вопрос многопоточности метода финализации. В HotSpot все методы finalize будут вызываться последовательно в одном потоке Finalizer. Однако, если вы вызовете метод System.runFinalization(), то родится еще один поток, который заблокирует текущий и будет выполнять методы finalize, если подходящие объекты есть в очереди. Причем это вполне может происходить параллельно основному потоку Finalizer.
  3. Переопределение метода finalize значительно удлиняет время жизни объекта после смерти, так как он будет удален из памяти не раньше второй сборки мусора. А учитывая два первых пункта, если метод finalize у вас будет тяжелым и\или таких объектов будет очень много, то объекты могут довольно долго висеть в фазе финализации и продолжать занимать место в памяти.
  4. Во время выполнения метода finalize вы можете восстановить ссылку на объект, например, поместив ее в какой-нибудь статический контекст, тем самым вы воскресите объект. Опасность такого маневра заключается в том, что второй раз метод finalize у данного объекта уже никогда вызван не будет. Поэтому если вам по каким-то причинам очень надо воскресить данный объект, то лучше создавайте внутри метода finalize его копию.
  5. Одна из самых неприятных проблем возникающих при использовании метода finalize — это реордеринг. Представьте, что у вас есть два объекта с переопределенным методом finalize, один из которых ссылается на другой. Так вот, если эти объекты стали недостижимы, то порядок вызова методов финализации произойдет в случайном порядке. Таким образом, у вас будет потенциальная опасность вызвать какой-нибудь метод на уже финализированном объекте из метода finalize другого объекта и получить ошибку. Причем проблема будет возникать не на каждом объекте, что добавит головной боли при отладке.
  6. Согласно Джошуа Блоху, автору знаменитой книги «Effective Java: Programming Language Guide», для объектов с переопределенным методом finalize аллокация и сборка может происходить в 430 раз медленнее, чем у обычного объекта.
  7. Любые исключения выброшенные в теле метода будут проигнорированы.
  8. Надо не забыть в конце метода вызвать super.finalize (). А учитывая предыдущий пункт, сделать это необходимо в блоке finally.

Согласно всему вышесказанному по возможности следует избегать использование метода finalize, вернее не стоит полагаться на него. Лучше освобождать ресурсы программно, а в методе finalize логировать, если этого почему-то сделано не было, чтобы вовремя найти и починить возникшую проблему.
Если же вам все же надо освободить ресурсы именно при сборке объекта, то, вероятнее всего для этого лучше использовать фантомные ссылки.
Тёма @javaspecialist
карма
67,2
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (19)

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

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

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

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

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


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

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

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

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

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

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

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.