Должен ли Java 9 Cleaner быть предпочтительным для завершения?

В Java переопределение метода finalize получает плохой рэп, хотя я не понимаю, почему. Классы, такие как FileInputStream используют его для обеспечения того, чтобы close вызывали FileInputStream как на Java 8, так и на Java 10. Тем не менее, Java 9 представил java.lang.ref.Cleaner который использует механизм PhantomReference вместо завершения GC. Сначала я подумал, что это просто способ добавить финализацию к сторонним классам. Однако пример, приведенный в его javadoc, показывает прецедент, который можно легко переписать с помощью финализатора.

Должен ли я переписывать все мои методы finalize с точки зрения Cleaner? (Разумеется, у меня их мало. Просто некоторые классы, которые используют ресурсы ОС, в частности, для взаимодействия CUDA).

Как я могу сказать, Cleaner (через PhantomReference) избегает некоторых опасностей finalizer. В частности, у вас нет доступа к очищенному объекту, и вы не можете его воскресить или какие-либо его поля.

Однако это единственное преимущество, которое я вижу. Чистота также нетривиальна. Фактически, он и финализация используют ReferenceQueue ! (Разве вам не нравится, как легко читать JDK?) Это быстрее, чем финализация? Не позволяет ли ждать двух GC? Избежит ли это изнурение кучи, если многие объекты поставлены в очередь для очистки? (Ответ всем тем, как мне кажется, не будет.)

Наконец, на самом деле ничего не гарантируется, чтобы остановить вас от ссылки на целевой объект в процессе очистки. Будьте внимательны, прочитайте длинную записку API! Если вы в конечном итоге ссылаетесь на объект, весь механизм будет тихо ломаться, в отличие от финализации, которая всегда пытается обмякнуть. Наконец, в то время как поток финализации управляется JVM, создание и хранение потоков Cleaner - ваша собственная ответственность.

Ответы

Ответ 1

Не используйте ни

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

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

Утечки ресурсов должны быть исправлены, а не устранены.

С этой целью я предлагаю взглянуть на полученный от Netty almson-refcount. Он предлагает эффективный детектор утечки ресурсов, основанный на слабых ссылках, и дополнительное средство подсчета ссылок, которое является более гибким, чем обычное автозаполнение. Что делает его детектор утечки отличным, так это то, что он предлагает разные уровни отслеживания (с разным объемом служебной информации), и вы можете использовать его для захвата следов стека, где размещены и использованы ваши просочившиеся объекты.

Ответ 2

Вы не должны заменять все методы finalize() на Cleaner. Тот факт, что устаревание метода finalize() и введение (public) Cleaner произошло в одной и той же версии Java, указывает только на то, что общая работа по этой теме произошла, а не то, что она должна быть заменой другой.

Другая связанная с этим работа Java-версия - это удаление правила, при котором PhantomReference автоматически не очищается (да, до Java 9 с использованием PhantomReference вместо finalize() прежнему требуется два цикла GC для восстановления объекта) и введение Reference.reachabilityFence(…).

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

Если ваш класс инкапсулирует ресурс без кучи, в документации указано:

Классы, чьи экземпляры содержат ресурсы без кучи, должны предоставить метод, позволяющий обеспечить явный выпуск этих ресурсов, и при необходимости они должны также реализовать AutoCloseable.

(так что это предпочтительное решение)

Cleaner и PhantomReference обеспечивают более гибкие и эффективные способы выделения ресурсов, когда объект становится недоступным.

Поэтому, когда вам действительно нужно взаимодействие с сборщиком мусора, даже в этом кратком комментариях к документации упоминаются две альтернативы, так как PhantomReference не упоминается здесь как скрытый от разработчика брандмауэр Cleaner; использование PhantomReference напрямую является альтернативой Cleaner, что может быть еще сложнее в использовании, но также обеспечивает еще больший контроль за временем и потоками, включая возможность очистки в том же потоке, который использовал ресурс. (Сравните с WeakHashMap, который имеет такую очистку, избегая затрат на потокобезопасные конструкции). Это также позволяет справляться с исключениями, брошенными во время очистки, лучшим способом, чем их молчаливо глотать.

Но даже Cleaner решает больше проблем, о которых вы знаете.

Существенной проблемой является время регистрации.

  • Объект класса с нетривиальным методом finalize() регистрируется при выполнении конструктора Object(). На данный момент объект еще не инициализирован. Если ваша инициализация завершена с исключением, метод finalize() все равно будет вызываться. Может возникнуть соблазн решить эту проблему данными объектов, например, установить для initialized флага значение true, но вы можете сказать это только для своих собственных данных экземпляра, но не для данных подкласса, которые еще не были инициализированы при возврате конструктора,

    Для регистрации уборщика требуется полностью сконструированный Runnable содержащий все необходимые данные для очистки, без ссылки на строящийся объект. Вы даже можете отложить регистрацию, когда распределение ресурсов не произошло в конструкторе (подумайте о несвязанном экземпляре Socket или Frame который не связан с атомом на дисплее)

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

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

  • Как вы уже обнаружили, существует метод clean(), который позволяет немедленно выполнить очистку и удалить очиститель. Поэтому, когда вы предоставляете явный метод закрытия или даже реализуете AutoClosable, это предпочтительный способ очистки, своевременного удаления ресурса и избавления от всех проблем очистки на основе сборщика мусора.

    Обратите внимание, что это согласуется с указанными выше пунктами. Для объекта может быть несколько очистителей, например, зарегистрировано разными классами в иерархии. Каждый из них может запускаться индивидуально, с внутренним решением, касающимся прав доступа, только тот, кто зарегистрировал уборщика, получает Cleanable к связанному с ним Cleanable чтобы иметь возможность вызвать метод clean().


Тем не менее, часто упускается из виду, что самое худшее, что может произойти при управлении ресурсами с сборщиком мусора, заключается не в том, что действие по очистке может выполняться позже или никогда вообще. Самое худшее, что может случиться, заключается в том, что он работает слишком рано. См., Например, finalize(), называемый объектом с высокой степенью достижимости в Java 8. Или, действительно хороший, JDK-8145304, Executors.newSingleThreadExecutor(). Submit (runnable) throws RejectedExecutionException, где финализатор отключает службу-исполнитель, которая все еще используется.

Конечно, просто использование Cleaner или PhantomReference не решает этого. Но удаление финализаторов и реализация альтернативного механизма, когда это действительно необходимо, - это возможность тщательно продумать тему и, возможно, вставить reachabilityFence случае необходимости. Самое худшее, что вы можете иметь, это метод, который выглядит как простой в использовании, когда на самом деле тема ужасно сложна, и 99% ее использования потенциально могут сломаться.

Кроме того, хотя альтернативы более сложны, вы сказали сами, они редко нужны. Эта сложность должна влиять только на часть вашей базы кода. Любой, почему должен java.lang.Object, базовый класс для всех классов, размещать метод, рассматривающий редкий угловой случай программирования Java?

Ответ 3

Как отметил Эллиотт в комментариях, продвигаясь вперед с Java9+, Object.finalize устарел, и поэтому имеет больше смысла для реализации методов с использованием Cleaner. Также из заметок о выпуске:

Метод java.lang.Object.finalize устарел. Механизм завершения по своей сути проблематичен и может привести к проблемам с производительностью, блокировкам и зависаниям. java.lang.ref.Cleaner и java.lang.ref.PhantomReference предоставляют более гибкие и эффективные способы освобождения ресурсов, когда объект становится недоступным.

Подробности в базе данных ошибок - JDK-8165641

Ответ 4

Java 9 Cleaner очень похож на традиционную финализацию (реализованную в OpenJDK), и почти все, что можно сказать о финализации (хорошо или плохо), можно сказать и о Cleaner. Оба полагаются на сборщик мусора для размещения объектов Reference в ReferenceQueue и используют отдельный поток для запуска методов очистки.

Три основных различия заключаются в том, что Cleaner использует PhantomReference вместо того, что по сути является WeakReference, использует отдельный поток для каждого очистителя с настраиваемой ThreadFactory и позволяет очищать (т.е. отменять) ссылки PhantomReferences вручную, и никогда не ставить в очередь.

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

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

Тем не менее, finalize устарела. Так что это, я думаю. Что-то вроде движения члена. Возможно, разработчики JDK думают, "почему JDK должен предоставить собственный механизм, который можно легко реализовать в виде библиотеки" "n00bs. N00bs везде. N00bs, прекратите использовать finalize, мы вас так ненавидим". Это хороший момент - и все же, я не могу представить, чтобы finalize действительно исчез. Это, вероятно, всегда будет работать, поэтому я не собираюсь прекращать его использовать.

Хорошую статью, в которой рассказывается о финализации и обрисовывается, как работает альтернативная финализация, можно найти здесь: Как обрабатывать проблемы сохранения памяти при финализации в Java Он подробно описывает работу Cleaner.

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

Netty ResourceLeakDetector намного круче, чем Cleaner.