0,0
рейтинг
14 июня 2013 в 13:39

Разработка → Как я пытался понять смысл метода finalize из песочницы

Недавно меня пригласили в одну компанию на собеседование на должность Java-программиста. На собеседовании зашла речь о работе метода finalize. Я имел лишь поверхностное представление о работе этого метода и не смог дать достойного его описания интервьюверам. Поэтому после собеседования я должен был провести работу над ошибками и во всем разобраться.

Мои знания ограничивались тем, что метод finalize вызывается в момент, когда сборщик мусора начинает утилизировать объект. И я не совсем понимал для чего он служит. Я думал, что это что-то типа деструктора, в котором можно освобождать определенные ресурсы после того, как они больше не нужны, причем даже ресурсы, которые хранятся в других объектах, что не верно.

Так вот, первое, что требовалось понять — назначение.

Предназначен этот метод для автоматического освобождения системных ресурсов, занимаемых объектом, на котором будет данный метод вызван. Это кажется удобным, чтобы не помнить постоянно, например, что мы должны закрыть соединение с каким-то ресурсом, когда оно больше не требуется.

Не стоит полагаться на finalize для чистки данных. Во-первых, нет гарантии, что он будет вызван, т.к. где-то может остаться ссылка на объект. Во-вторых, нет гарантии на то, в какое время будет вызван метод. Это связано с тем, что после того, как объект становится доступным для сборки и, если в нем переопределен метод finalize, то он не вызывается сразу, а помещается в очередь, которая обрабатывается специально созданным для этого потоком. Стоит отметить, что в очередь на финализацию попадают только те объекты, в которых переопределен метод finalize.

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

Интересной особенностью метода является то, что он может снова сделать объект доступным, присвоив this какой-нибудь переменной, хотя так делать не рекомендуется, т.к. при восстановлении объекта, повторно finalize вызван не будет

Может случиться еще один редкий момент. У нас есть класс A, в котором реализован метод finalize. Мы создаем класс B extends A, в котором забываем про finalize. Объекты класса B содержат в себе много данных. Когда объекты классы B становятся ненужными, они попадут в очередь на финализацию и определенное время еще будут занимать память, вместо того, чтобы миновать этой очереди и сразу утилизироваться.

Еще одним недостатком является то, что надо помнить про вызов finalize-метода супер-класса, если мы переопределяем его. Разработчик не вызовет — никто не вызовет.

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

Есть один способ быть уверенным, что finalize-методы были запущены для объектов, доступных для сборки: вызвать System.runFinalization() или Runtime.getRuntime().runFinalization(). Выход из метода осуществляется только тогда, когда все доступные методы объектов для финализации будут выполнены

Для себя я сделал вывод, что пользоваться этим методом без особой надобности не стоит, а случаи этой особой надобности на моей двух-с-половиной-летней практике пока не встречались.

Лучше вместо finalize писать методы типа close в java.io и вызывать их в блоке finally. Недостатком является то, что разработкик должен помнить, что ресурс после использования нужно закрыть. На помощь тут нам пришла Java SE 7 со своими try-with-resources

Но ведь этот метод для чего-то есть. Где и как его можно использовать? Есть ли примеры использования?

Finalize можно использовать как последний шанс закрыть ресурс, но никогда как первая или единственная попытка. Т.е. в дополнение к тому, что клиент может вызвать, например, метод close на объекте, представляющем ресурс. А может и забыть. Тогда можно попытаться ему помочь. Так сделано, например, в классе FileInputStream.java:

protected void finalize() throws IOException {
    if ((fd != null) &&  (fd != FileDescriptor.in)) {
        /*
         * Finalize should not release the FileDescriptor if another
         * stream is still using it. If the user directly invokes
         * close() then the FileDescriptor is also released.
         */
        runningFinalize.set(Boolean.TRUE);
        try {
            close();
        } finally {
            runningFinalize.set(Boolean.FALSE);
        }
    }
}


Данный подход часто используется в библиотеках Java.

PS. Прошу не принимать написанное за чистую правду. Я написал лишь то, как я понял прочитанный материал. Приму с радостью любые дополнения, критику и исправления.

Список использованной литературы:
How to Handle Java Finalization's Memory-Retention Issues
Java Finalize method call
java.lang. Class Object
10 points on finalize method in Java – Tutorial Example
Habrahabr. Finalize и Finalizer
Александр Дмитриев @shurik2533
карма
61,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

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

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

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

  • +16
    Интересно, если я на собеседовании скажу, что это вредный и опасный костыль, который своим существованием сбивает с толку, то буду прав?
    • НЛО прилетело и опубликовало эту надпись здесь
      • +3
        А тем, что теряется однозначность. Мне кажется, что в этом методе можно разве что кеши и временные файлы подчищать, которые не влияют на работу уже по факту.
        Иначе если возникает потребность в этом методе, то это баг архитектуры.
        • –1
          Я ниже привёл пример с закрытием соединения к базе данных.
          • +4
            Так это же и есть классический костыль из палаты мер и весов.
            • –1
              Ну, костылёк, конечно… Ну а как Вы предлагаете эту проблему решить иначе?
              • +2
                RAII.
                • –1
                  И как это может быть имплементировано в Яве?
                  • +1
                    Я не сильно большой специалист по Яве, но помню люди что-то на тему try with resources говорили.
                    • –1
                      Оно для такой ситуации не очень подойдёт, поскольку открытие ресурса, работа и закрытие ресурса могут быть в этом случае сильно разнесены по коду.
    • 0
      зависит от собеседующего с Вами.
    • 0
      Насколько ячитал Хростманна, finalize() необходим при работе с нативным кодом и освобождения ресурсов там, а исключить вероятность работы Java с С-кодом, например, нельзя. Как Вы решили бы проблему освобождения ресурсов, созданных при работе с не Java-кодом внутри Java -кода?
  • +30
    finalize в Яве нужен, чтобы его не использовать.
  • +12
    У Джошуа Блоха хорошо написано об этом методе: link
    Краткая выдержка:
    1. finalize() можно использовать только в двух случаях:
    1.1. Проверка/подчистка ресурсов с логированием
    1.2. При работе с нативным кодом, который не критичен к утечке ресурсов
    2. finalize() замедляет работу GC по очистке объекта в 430 раз
    3. finalize() может быть не вызван

    Собственный опыт — за пять лет ни разу не переопределял finalize(). Собственно, переопределение finalize() — признак smelly кода.
  • 0
    Исключения, брошенные в методе finalize, не обрабатываются потоком-финализатором, т.е. данный стектрейс скорее всего нельзя будет отследить.
    Есть такая хорошая штука, называется UncaughtExceptionHandler и его можно проставить потоку, группе потоков или всему приложению — так вы решите проблему всех непойманных эксепшнов.

    А смысла нет — это неудачный костыль, который не удаляют из-за совместимости с прошлыми версиями.
    • +3
      Интересно, а почему он тогда до сих пор не deprecated?
      • 0
        Подумавши, родил два варианта:
        а) Создатели верят, что этот метод будет еще приносить пользу тем глупцам, которые забывают сами освобождать ресурсы
        б) Не красиво, когда в святая-святых — классе Ojbect будет висеть метод @Deprecated
      • +1
        Потому что deprecated нужно удалить в течение 2 релизов, а сабж не удалят никогда, даже если он устарел?
        • +2
          Stop и resume в классе Thread deprecated начиная с Java 1.3, и ничего.
    • 0
      Вполне себе нормальная вещь, большинство сборщиков ошибок вешаются на поток как UncaughtExceptionHandler и репортят исключения на сервер.
  • +3
    PhantomReference
  • +4
    Не разу не использовал finalize и не собираюсь. Тот кто не закрываает ресурсы при помощи try-finally сам себе злобный Буратино.
  • 0
    Единственный смысл finalize, который я знаю — последняя линия обороны против чужих ошибок. К примеру, я создаю класс, который использует соединение с каким-то внешним ресурсом (ну, скажем, с базой данных). Предполагается, что использоваться он будет примерно так:

    MyClass c = new MyClass();
    c.openConnection();
    c.doSomething();
    c.doSomethingElse();

    c.doOneMoreThing();
    c.closeConnection();

    Я не могу гарантировать, что тот, кто будет пользоваться классом, не забудет закрыть соединение. Поэтому, на самый крайний случай, я могу добавить закрытие соединения в finalize в своем классе.
    • –2
      Мне кажется, что это не правильно — усложнять свой код, что бы кто-то подобрать за кем-то. Тогда весь код нужно обвешать if'ами, который прощитывает все остальные невалидные инварианты помимо одного валидного(так называемая защита от дурака).

      ИМХО Код должен работать только так, как он должен работать. В остальных случаях он должен либо падать либо работать не правильно. Если пишите либу — нужно писать об этом в Faq/туториале, том же Javadoc. Если человек не умеет/не хочет читать документацию/вики/туториал, то это его проблемы.

      Ведь это и так достаточно нетривиальная задача — написать код который просто работает(как бы смешно это не звучало).
      • +1
        Я всегда стараюсь писать код, который будет работать — хоть как-то — даже если его используют неправильно. В конце концов, задача ведь — сделать, чтоб работало, и позиция «Прокукарекал — а там хоть не рассветай» не очень конструктивна.

        Конечно, если код видит, что что-то идёт не так, он должен сообщить об этом. Ну, скажем, сказать" конфигурация не найдена — использую значения по умолчанию". Поэтому в моём примере финализатор, если он обнаружит незакрытое соединение, попробует ещё и записать в лог.

        Код, работающий неправильно — это плохо, вне зависимости от того, кто в этом виноват. В моём же примере неправильная работа кода приводит к утечке ресурса, а это ошибка гадкая, трудноуловимая, и на продакшене может попортить немало крови.У меня в своё время был именно такой случай — из-за изменения в логике вкралась ошибка, и иногда управление не попадало на тот кусок, который закрывал соединение с базой. В результате начались падения программы из-за того, что закончились соединения. Происходило это не всегда, в совершенно ином куске программы, и нам потребовалось немало времени на то, чтобы поймать ошибку.
        • +6
          Моя практика показывает, что код работающий «хоть как-то» именно так и работает «хоть как-то». Вместо того, что бы сообщить явно и потратить пол дня на фиксинг этого бага — на последующий дебаг проблемы тратятся месяцы.

          >> Код, работающий неправильно — это плохо, вне зависимости от того, кто в этом виноват.
          Я с этим утверждением полностью согласен. Именно поэтому я придерживаюсь философии «Let it crash» — это позволяет узнать о проблеме намного раньше. Не работающий код должен кричать об этом во все горло, а не «тихо неправильно работать».

          Вот ведь признайтесь — если код будет работать неправильно на продакшне — как скоро вы об этом узнаете? В случае «exception'a» я думаю к вам сразу прибегут. В случае логирования с уровнем «error» и продолжающей неправильно работать системе — к вам прибегут намного позже.
          Так что на мой взгляд бОльшее зло позволять коду работать тихо неправильно.

          >> из-за изменения в логике вкралась ошибка
          Я понимаю, что такие ошибки очень неприятные, но я подобного типа проблемы стараюсь решать другими путями, если это возможно. Понятно, что есть когда «по-другому вообще никак». Но на самом-то деле если соединения «текут», то соответственно и производительность всей системы тоже падает.
          • –1
            Я с этим утверждением полностью согласен. Именно поэтому я придерживаюсь философии «Let it crash» — это позволяет узнать о проблеме намного раньше. Не работающий код должен кричать об этом во все горло, а не «тихо неправильно работать».

            С точки зрения бизнеса такая позиция далеко не всегда оправдана. Ошибки бывают разные. Понятно, что если код, к примеру, тихой сапой портит базу данных, то это никуда не годится. Но вот другой пример (тоже из личной практики): код должен прочитать файл с кучей записей транзакций, провести некие действия и выдать отчёт. В результате некоей неразберихи в какой-то момент на вход пошли файлы, в которых дата была не в том формате. Если бы программа свалилась, мы получили бы возмущённое начальство, бессонную ночь или две, а также всякие корпоративно-политические неприятности. Но программа была написана так, что она не упала, а написала подробный отчёт об ошибках, отправила его по почте, а сама вместо даты взяла текущую. Пользователи посмотрели с утра (программа ночью работала) и сказали: «Ну и ничего страшного. Вот у таких и таких транзакций поменяйте дату на вчерашнюю, а так всё нормально». Днём мы спокойно поправили формат и вопрос был закрыт.

            А с логами у меня было так: все ошибки из логов автоматически присылались по почте всеё команде, и непременно разбирались. Так что если бы та библиотека, которой мы пользовались, продиагностировала течь и сообщила об этом, мы бы непременно эту ошибку внесли в план исправлений, и исправили бы достаточно скоро.
            • 0
              Факт того, что ваш парсер логов собирает ошибки и отправляет автоматически всем разработчикам на электронную почту — это и есть «кричать во все горло».

              Я имел счастье саппортить систему написанную несколькими поколениями разработчиков. И знаете — насмотрелся на «код, который как-то работает». И в долгосрочной перспективе получается система, которая слабо поддается отладке.

              Другой пример из моей практикой, схожий с вашим — есть калькулятор финансов, который выдает расчет работнику кредитования о кредитной истории в виде PDF документа. И вот тоже ошибка «логировалась» о том, что часть данных не удается загрузить с внешнего сервиса. Но это успешно проигнорировали. И так проработало пару недель, пока не заметили случайно на продакшне. За этот срок было выдано много кредитов. И неизвестно какой урон понесет фирма после такого в долгосрочной перспективе.

              Так что в долгосрочной перспективе для фирмы лучше один раз получить по шапке за оплошность.
              • 0
                «Как-то» не означало «кое-как» :)

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

                В конце концов, мы не стали бы пользоваться, скажем, автомобилем, который отказывается ехать оттого, что у него перегорела одна фара.
                • +1
                  Да. Но вы согласны, что при неудачном стечении обстоятельств(темная ночь, плохая видимость) эта негорящая фара может привести к аварии и с точки зрения безопасности было бы разумнее поменять фару?
                  • +1
                    P.S. И пример с кредитованием приведенный выше еще неизвестно как выльется через несколько лет. А все потому, что один из программистов не подумал заранее и позволил продолжить ехать с по его мнению «выключенной фарой», хотя на самом деле это были «сломаные тормоза».
                    • 0
                      Через несколько лет и системы, возможно, не будет.
                      • 0
                        Если та контора разорится, то да — не будет :)
                    • +1
                      Я работал в компании, которая писала ПО для работы с людьми и их деньгами.
                      Увидел один раз код, который копипастом распространился везде: если по какой-то «счастливой» случайности не нашли клиента по айди — брался первый попавшийся. Снимались деньги или начислялся долг. Вот как надо начальства бояться )))
                      Возможно такого никогда не произошло. Но заботливый программист это заложил. И коду не один год…

                      Выживаемость программы — прямо противоположная вещь надежности в большинстве случаев.

                      Еще нравится, как люди пишут кругом, не задумываясь:
                      if (a != null)
                      {
                          a.DoSomething();
                      }
                      

                      И даже средства улучшения кода в полуавтоматическом режиме такие вещи делают. Ну, вы поняли…
                  • 0
                    Конечно, согласен, кто ж спорит. И хорошо, если автомобиль мигает по этому поводу лампочкой на панели, пищит, напоминает. Но ездить он всё равно должен.
                    • +1
                      Это опасное заблуждение, что автомобиль едет. В большинстве случаев. Лучше перебдеть. По умолчанию желательно считать, что если происходит что-либо неожиданное — контроль над программой утерян и она находится в несогласованном состоянии. Поэтому ее работа должна приравниваться к неработе. Причем, дальнейшая ее работа опаснее неработы, т.к. способствует «скрыванию» проблемы и увеличению убытков.

                      Природа бага такова, что вы, пока не создав тесты, стабильно его повторяющие, не нашли код и не исправили — причину бага не знаете. Не зная причины, вы никак не можете оценить важность бага и его урон.

                      Еще, в поддержку того, что лучше упасть — хотя гарантии отсутствия багов не будет никогда, если нормально написан код, покрыт тестами, багов на самом деле не очень много. Поэтому чем жестче программа реагирует на «неожиданное поведение», тем быстрее эти баги отловятся. Возможно даже до продакта.

                      Только в крайне редких случаях можно так делать — ехать дальше. Не хочется о них и напоминать. Бывает, что у вас приложение для какой-то сферы деятельности, а на ГУИ появляются непонятные артефакты или что-то несущественное не прорисовывается. Или действительно обрабатываете данные с мусором и это ожидаемое поведение, и вы логируете предупреждения. Т.е. очень иногда так можно поступать. К сожалению, многие это «иногда» возводят в абсолют.
            • +3
              Почему-то припомнилось это
        • +1
          От этого технический долг растет. На первое время, когда пилишь прототип, такое сгодится, но потом это становится похоже на «лечение» кариеса обезболивающим.
          • 0
            Ну, в данном случае это скорее не долг, а кредит. Возможность оплатить счёт позже, когда будут деньги :)

            Кстати, аналогия на самом деле неплохая: при злоупотреблении такой политикой возможно техническое банкротство. Однако при разумном использовании всё получается очень хорошо. Мы, например, периодически проводили такие релизы с «раздачей долгов», стараясь назначить их на спокойное время.
        • 0
          паосер хабра глючит, удалено
      • 0
        паосер хабра глючит, удалено
        • +1
          Даже если данный подход был бы признан неправильным/устаревшим наврядли Java избавится от метода finalize.

          Это как с багом в базе данных Oracle — там пустая строка и null это одно и тоже. Баг смешной, но от него никто никогда не избавится по понятным причинам :)
    • 0
      А мне кажется, что пусть он лучше словит ошибку, чем будет неправильно делать и дальше в полной уверенности, что он все делает красиво и круто.
      • 0
        Я в ветке выше привёл пару иллюстраций того, почему это далеко не всегда так… Для полноты картины поясню, что описываемые ситуации происходили в крупной финансовой компании. Стоимость того, что система упала и не смогла в нужное время выполнить требуемую операцию, там может быть высока. ОЧЕНЬ высока.

        Стоимость того, что из-за течи замедляется работа сервера и его, пока не починили, время от времени нужно перезапустить — ничтожна. Стоимость неполного отчёта может тоже быть велика, но она меркнет на фоне стоимости застрявшей ночной обработки данных.
        • 0
          Тогда пусть на тестовом сервере отключают финализаторы, а включают их только на продакшене.
          • +1
            Отключение их поможет плохо — пул соединений может в тестовых условиях и не истощиться. Гораздо лучше на тестовом сервере из финализатора, если там есть незакрытое соединение, просто ронять программу. С этим я на сто процентов согласен.
        • +1
          Я думаю вы согласитесь в том, что тот факт, что вам в финализаторы пришлось логику пихать — это следствие толи низкой внимательности программистов, толи низкой квалификации программиста. В общем это следствие чьей-то ошибки. И это цена, которую пришлось вам заплатить за чью-то лень/неванимателность/тупость.
          • 0
            Это попытка предотвратить нехорошие поледствия чужих ошибок. Да, это так. Но если Вы посмотрите вокруг, то Вы увидите что так построено огромное количество вещей, которые нас окружают. Лифт откроет обратно двери, если кто-то сунул в последний момент внутрь руку; кофемолка не запустится с незакрытой крышкой; предохранитель сработает, если воткнуть в розетку обогреватель на 20 киловатт… Так что ничего странного или необычного в этом нет.

  • +1
    В каментах к подобной статье от меня я упоминал один более-менее вменяемый вариант использования finalize. Если объекты держатся в кэше на weak/soft-ссылках, то проще очистку системных ресурсов запихать в finalize, чем самому возиться с PhantomReference. Впрочем, многим такой пример покажется искусственным.
  • 0
    Использовал finalize, но только лишь для одной цели… когда искал возможные причины утечки памяти, добавлял в finalize логирование. Другого применения для этого метода в реальном проекте не нашел.

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