Переопределение Object.finalize () действительно плохо?

34

Основными двумя аргументами против переопределения Object.finalize()является то, что:

  1. Вы не можете решить, когда это называется.

  2. Это не может быть вызвано вообще.

Если я правильно понимаю, я не думаю, что это достаточно веские причины, чтобы Object.finalize()так сильно ненавидеть .

  1. Именно реализация виртуальной машины и GC должны определить, когда подходящее время для освобождения объекта, а не разработчик. Почему так важно решить, когда вам Object.finalize()позвонят?

  2. Обычно, и поправьте меня, если я ошибаюсь, единственное время Object.finalize(), когда меня не вызвали, - это когда приложение было закрыто до того, как GC получил шанс на запуск. Однако объект все равно освобождается, когда процесс приложения завершается с ним. Так Object.finalize()что не позвонили, потому что не нужно было звонить. Почему разработчик заботится?

Каждый раз, когда я использую объекты, которые мне приходится закрывать вручную (например, дескрипторы файлов и соединения), я очень расстраиваюсь. Я должен постоянно проверять, есть ли у объекта реализация close(), и я уверен, что пропустил несколько обращений к нему в некоторые моменты в прошлом. Почему не проще и безопаснее просто предоставить виртуальной машине и сборщику мусора распоряжение этими объектами путем внедрения close()реализации Object.finalize()?

AxiomaticNexus
источник
1
Также обратите внимание: как и многие API из эпохи Java 1.0, семантика потоков finalize()немного запутана. Если вы когда-либо реализуете его, убедитесь, что он поточно-ориентирован по отношению ко всем остальным методам того же объекта.
billc.cn
2
Когда вы слышите, как люди говорят, что финализаторы плохие, это не значит, что ваша программа перестанет работать, если они у вас есть; они означают, что сама идея завершения довольно бесполезна.
user253751
1
+1 к этому вопросу. В большинстве ответов ниже указано, что ресурсы, такие как файловые дескрипторы, ограничены и их следует собирать вручную. То же самое было / верно в отношении памяти, поэтому, если мы принимаем некоторую задержку в сборе памяти, почему бы не принять ее для файловых дескрипторов и / или других ресурсов?
Мбоннин
Обращаясь к последнему абзацу, вы можете оставить его на Java для обработки закрывающих вещей, таких как файловые дескрипторы и соединения, с минимальными усилиями с вашей стороны. Используйте блок try-with-resources - он упоминался несколько раз уже в ответах и ​​комментариях, но я думаю, что это стоит поставить здесь. Учебное пособие по Oracle можно найти по адресу docs.oracle.com/javase/tutorial/essential/exceptions/…
Jeutnarg

Ответы:

45

По моему опыту, есть одна и только одна причина переопределения Object.finalize(), но это очень веская причина :

Чтобы разместить код регистрации ошибок, в finalize()котором уведомляет вас, если вы когда-нибудь забудете вызвать close().

Статический анализ может выявлять пропуски только в сценариях тривиального использования, а предупреждения компилятора, упомянутые в другом ответе, имеют настолько упрощенное представление о вещах, что вам действительно нужно отключить их, чтобы выполнить что-то нетривиальное. (У меня включено намного больше предупреждений, чем у любого другого программиста, о котором я знаю или когда-либо слышал, но у меня не включены глупые предупреждения.)

Финализация может показаться хорошим механизмом для обеспечения того, чтобы ресурсы не оставались нераспределенными, но большинство людей воспринимают это совершенно неправильно: они думают об этом как о альтернативном резервном механизме, защите «второго шанса», которая автоматически спасет день, избавившись от ресурсов, которые они забыли. Это совершенно неправильно . Должен быть только один способ сделать любую вещь: либо вы всегда закрываете все, либо финализация всегда закрывает все. Но поскольку завершение ненадежно, оно не может быть завершением.

Итак, есть эта схема, которую я называю Mandatory Disposal , и она предусматривает, что программист всегда отвечает за явное закрытие всего, что реализует Closeableили AutoCloseable. (Оператор try-with-resources по-прежнему считается явным закрытием.) Конечно, программист может забыть, так что вот когда финализация вступает в игру, но не как волшебная фея, которая волшебным образом исправит ситуацию в конце: если финализация обнаружит это close()не было вызвано, это непопытаться вызвать его именно потому, что (с математической уверенностью) найдутся орды программистов n00b, которые будут полагаться на него, чтобы выполнять работу, которую они слишком ленивы или слишком рассеянны. Таким образом, при обязательном удалении, когда финализация обнаруживает, что close()не было вызвано, она записывает ярко-красное сообщение об ошибке, сообщая программисту большими жирными заглавными буквами, чтобы он исправил свои ошибки.

В качестве дополнительного преимущества, ходят слухи, что «JVM будет игнорировать тривиальный метод finalize () (например, метод, который просто возвращает, ничего не делая, как тот, который определен в классе Object)», поэтому при обязательном удалении вы можете избежать всей финализации издержки во всей вашей системе ( см. ответ alip для получения информации о том, насколько ужасны эти издержки), кодируя ваш finalize()метод следующим образом:

@Override
protected void finalize() throws Throwable
{
    if( Global.DEBUG && !closed )
    {
        Log.Error( "FORGOT TO CLOSE THIS!" );
    }
    //super.finalize(); see alip's comment on why this should not be invoked.
}

Идея заключается в том, что Global.DEBUGэто static finalпеременная, значение которой известно во время компиляции, поэтому, если это так, falseто компилятор вообще не будет генерировать никакого кода для всего ifоператора, что сделает этот тривиальный (пустой) финализатор, который, в свою очередь, означает, что ваш класс будет обрабатываться так, как будто у него нет финализатора. (В C # это было бы сделано с хорошим #if DEBUGблоком, но что мы можем сделать, это Java, где мы платим очевидную простоту в коде с дополнительными издержками в мозге.)

Подробнее о принудительном удалении, с дополнительным обсуждением об утилизации ресурсов в dot Net, здесь: michael.gr: Обязательное удаление против мерзости "избавляйся от утилизации"

Майк Накис
источник
2
@MikeNakis Не забывайте, что Closeable определяется как ничего не делать, если вызывается второй раз: docs.oracle.com/javase/7/docs/api/java/io/Closeable.html . Я признаю, что иногда я записывал предупреждение, когда мои классы закрыты дважды, но технически вы даже не должны этого делать. Технически, хотя, вызов .close () для Closable более одного раза совершенно допустим.
Патрик М
1
@usr все сводится к тому, доверяете ли вы тестированию или не доверяете тестированию. Если вы не доверяете своему тестирования, конечно, идти вперед и страдать над головой финализации для также close() , на всякий случай. Я считаю, что если моим испытаниям нельзя доверять, то лучше не выпускать систему в производство.
Майк Накис
3
@Mike, чтобы if( Global.DEBUG && ...конструкция работала так, чтобы JVM игнорировала finalize()метод как тривиальный, Global.DEBUGдолжна быть установлена ​​во время компиляции (в отличие от вставленной и т. Д.), Поэтому последующий код будет мертвым. Вызова super.finalize()вне блока if также достаточно, чтобы JVM восприняла его как нетривиальное (независимо от значения Global.DEBUG, по крайней мере, в HotSpot 1.8), даже если суперкласс #finalize()тривиален!
алип
1
@ Майк, боюсь, это так. Я протестировал его с помощью (слегка измененной версии) теста в статье, на которую вы ссылались , и подробный вывод GC (вместе с удивительно низкой производительностью) подтвердил, что объекты копируются в пространство оставшихся в живых / старого поколения и нуждаются в GC с полной кучей, чтобы получить избавиться от.
алип
1
Часто упускают из виду риск преждевременного сбора объектов, что делает освобождение ресурсов в финализаторе очень опасным действием. До Java 9 единственный способ убедиться, что финализатор не закрывает ресурс, пока он еще используется, - это синхронизировать объект как в финализаторе, так и в методах, использующих ресурс. Вот почему это работает в java.io. Если такого типа безопасности потока не было в списке пожеланий, это добавляет к накладным расходам, вызванным finalize()
Хольгер
28

Каждый раз, когда я использую объекты, которые мне приходится закрывать вручную (например, дескрипторы файлов и соединения), я очень расстраиваюсь. [...] Почему не проще и безопаснее просто оставить виртуальной машине и GC распоряжаться этими объектами, вставляя close()реализацию Object.finalize()?

Поскольку файловые дескрипторы и соединения (то есть файловые дескрипторы в системах Linux и POSIX) являются довольно редким ресурсом (в некоторых системах вы можете ограничиться 256 из них или 16384 в других; см. Setrlimit (2) ). Нет гарантии, что GC будет вызываться достаточно часто (или в нужное время), чтобы избежать исчерпания таких ограниченных ресурсов. И если GC недостаточно вызван (или финализация не выполняется в нужное время), вы достигнете этого (возможно, низкого) предела.

Финализация - это «лучшее из возможного» в JVM. Он может не вызываться или вызываться слишком поздно ... В частности, если у вас много ОЗУ или если ваша программа не выделяет много объектов (или если большинство из них умирает до того, как их перенаправляют на достаточно старый) генерация копирующим поколением GC), GC можно вызывать довольно редко, и финализация может выполняться не очень часто (или даже вообще не запускаться).

Так что closeфайловые дескрипторы явно, если это возможно. Если вы боитесь их утечки, используйте финализацию как дополнительную меру, а не как основную.

Василий Старынкевич
источник
7
Я бы добавил, что закрытие потока в файл или сокет обычно сбрасывает его. Оставлять открытыми потоки без необходимости увеличивают риск потери данных в случае отключения питания,
2
На самом деле, хотя количество разрешенных файловых дескрипторов может быть небольшим, это не очень сложный вопрос, потому что его можно использовать как сигнал для ГХ, по крайней мере. Действительно проблематично, что это а) совершенно непонятно для GC, сколько ресурсов, не управляемых GC, висит на нем, и б) многие из этих ресурсов уникальны, поэтому другие могут быть заблокированы или запрещены.
Дедупликатор
2
И если вы оставите файл открытым, это может помешать кому-то другому использовать его. (Средство просмотра Windows 8 XPS, я смотрю на вас!)
Лорен Печтел
2
«Если вы боитесь утечки [файловых дескрипторов], используйте финализацию как дополнительную меру, а не как основную». Это утверждение звучит подозрительно для меня. Если вы разрабатываете свой код хорошо, стоит ли вводить избыточный код, который распространяет очистку по нескольким местам?
Mucaho
2
@BasileStarynkevitch Ваша точка зрения заключается в том, что в идеальном мире избыточность - это плохо, но на практике, где вы не можете предвидеть все соответствующие аспекты, лучше быть в безопасности, чем сожалеть?
Муахо,
13

Посмотрите на ситуацию следующим образом: вы должны писать только код, который (а) является правильным (в противном случае ваша программа просто ошибочна) и (б) необходим (в противном случае ваш код слишком велик, что означает, что необходимо больше ОЗУ, больше циклов, потраченных на бесполезные вещи, больше усилий, чтобы понять это, больше времени, потраченного на поддержание этого и т. д. и т. д.)

Теперь рассмотрим, что именно вы хотите сделать в финализаторе. Либо это необходимо. В этом случае вы не можете поместить его в финализатор, потому что вы не знаете, будет ли он вызван. Это не достаточно хорошо. Или это не нужно - тогда вам не следует писать это в первую очередь! В любом случае, поместить его в финализатор - неправильный выбор.

(Обратите внимание, что примеры, которые вы называете, например закрытие файловых потоков, выглядят так, как будто они на самом деле не нужны, но они есть. Просто пока вы не достигнете предела количества открытых дескрипторов файлов в вашей системе, вы не заметите, что ваши код неверен. Но это ограничение является особенностью операционной системы и , следовательно , еще более непредсказуемым , чем политики JVM для финализаторов, так что на самом деле, на самом деле это важно , чтобы не тратить дескрипторы файлов.)

Килиан Фот
источник
Если я только должен писать код, который «необходим», тогда мне следует избегать всех стилей во всех моих приложениях с графическим интерфейсом? Конечно, ничего из этого не нужно; GUI будут работать без стилей, они будут выглядеть ужасно. Что касается финализатора, может быть необходимо что-то сделать, но все же можно поместить его в финализатор, поскольку финализатор будет вызываться во время объекта GC, если вообще будет. Если вам нужно закрыть ресурс, особенно когда он готов к сборке мусора, тогда финализатор является оптимальным. Либо программа прекращает выпуск вашего ресурса, либо вызывается финализатор.
Крюв
Если у меня есть тяжелый, дорогой для создания, закрываемый объект в ОЗУ, и я решил слабо ссылаться на него, чтобы его можно было очистить, когда он не нужен, тогда я мог бы использовать его finalize()для закрытия в случае, если цикл gc действительно нуждается в освобождении БАРАН. В противном случае, я бы держал объект в оперативной памяти вместо того, чтобы восстанавливать и закрывать его каждый раз, когда мне нужно его использовать. Конечно, ресурсы, которые он открывает, не будут освобождены, пока объект не будет GCed, когда бы это ни было, но мне, возможно, не нужно гарантировать, что мои ресурсы будут освобождены в определенное время.
Крюв
8

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

Кроме того, использование try-with-resources позволяет легко закрывать закрываемые объекты по завершении. Если вы не знакомы с этой конструкцией, я предлагаю вам проверить ее: https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html

Собаки
источник
8

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

Из теории и практики Java: сборка мусора и производительность (Брайан Гетц), финализаторы не ваш друг :

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

ALIP
источник
Отличный момент. Посмотрите мой ответ, который позволяет избежать снижения производительности finalize().
Майк Накис
7

Моя (наименьшая) любимая причина избегать Object.finalize- не то, что объекты могут быть завершены после того, как вы ожидаете этого, но они могут быть завершены до того, как вы ожидаете этого. Проблема не в том, что объект, который все еще находится в области видимости, может быть завершен до выхода из области действия, если Java решит, что он больше недоступен.

void test() {
   HasFinalize myObject = ...;
   OutputStream os = myObject.stream;

   // myObject is no-longer reachable at this point, 
   // even though it is in scope. But objects are finalized
   // based on reachability.
   // And since finalization is on another thread, it 
   // could happen before or in the middle of the write .. 
   // closing the stream and causing much fun.
   os.write("Hello World");
}

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

Майкл Андерсон
источник
1
Дело в том, что HasFinalize.streamсам по себе должен быть отдельно завершаемым объектом. То есть завершение HasFinalizeне должно завершаться или пытаться очистить stream. Или, если это должно, то это должно сделать streamнедоступным.
отлично
1
Это еще хуже
Хольгер
4

Я должен постоянно проверять, есть ли у объекта реализация close (), и я уверен, что пропускал несколько обращений к нему в некоторые моменты в прошлом.

В Eclipse я получаю предупреждение всякий раз, когда забываю закрыть то, что реализует Closeable/ AutoCloseable. Я не уверен, является ли это Eclipse или частью официального компилятора, но вы можете использовать аналогичные инструменты статического анализа, чтобы помочь вам в этом. Например, FindBugs может помочь вам проверить, не забыли ли вы закрыть ресурс.

Небу Покинс
источник
1
Хорошая идея упоминания AutoCloseable. Это упрощает управление ресурсами с помощью try-with-resources. Это сводит на нет несколько аргументов в вопросе.
2

На ваш первый вопрос:

Именно реализация виртуальной машины и GC должны определить, когда подходящее время для освобождения объекта, а не разработчик. Почему так важно решить, когда вам Object.finalize()позвонят?

Что ж, JVM определит, когда стоит восстановить память, выделенную для объекта. Это не обязательно время, когда finalize()должен произойти очистка ресурса, в котором вы хотите выполнить . Это проиллюстрировано в вопросе «finalize (), вызываемый для сильно достижимого объекта в Java 8» в SO. Там close()метод был вызван finalize()методом, в то время как попытка чтения из потока тем же объектом все еще ожидает. Таким образом, помимо известной возможности, которая finalize()вызывается слишком поздно, есть вероятность, что она вызывается слишком рано.

Предпосылка вашего второго вопроса:

Обычно, и поправьте меня, если я ошибаюсь, единственное время Object.finalize(), когда меня не вызвали, - это когда приложение было закрыто до того, как GC получил шанс на запуск.

это просто неправильно. Для JVM вообще не требуется поддержка финализации. Что ж, это не совсем неправильно, так как вы все равно можете интерпретировать это как «приложение было прервано до его завершения», предполагая, что ваше приложение когда-либо прекратит работу.

Но обратите внимание на небольшую разницу между «GC» вашего первоначального утверждения и термином «завершение». Сборка мусора отличается от завершения. Как только управление памятью обнаруживает, что объект недоступен, он может просто восстановить свое пространство, если либо у него нет специального finalize()метода, либо финализация просто не поддерживается, либо он может поставить объект в очередь для завершения. Таким образом, завершение цикла сборки мусора не означает, что финализаторы выполняются. Это может произойти позже, когда очередь обрабатывается, или вообще не обрабатывается.

Этот момент также является причиной того, что даже на JVM с поддержкой финализации полагаться на него для очистки ресурсов опасно. Сборка мусора является частью управления памятью и, следовательно, запускается потребностями в памяти. Вполне возможно, что сборка мусора никогда не запускается, потому что в течение всего времени выполнения достаточно памяти (ну, это все равно вписывается в описание «приложение было прервано до того, как GC получил шанс запустить»). Также возможно, что GC действительно работает, но после этого освобождается достаточно памяти, поэтому очередь финализатора не обрабатывается.

Другими словами, нативные ресурсы, управляемые таким образом, все еще чужды управлению памятью. Хотя гарантируется, что an генерируется OutOfMemoryErrorтолько после достаточных попыток освободить память, это не относится к собственным ресурсам и завершению. Возможно, что открытие файла завершится неудачно из-за недостатка ресурсов, когда очередь финализации заполнена объектами, которые могут освободить эти ресурсы, если финализатор когда-либо запускался…

Holger
источник