Следует ли предпочесть Java 9 Cleaner финализации?

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

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

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

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

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


person Aleksandr Dubinsky    schedule 18.10.2018    source источник
comment
Наверное. Java 9 устарела finalize() . В Javadoc явно говорится, что Cleaner и PhantomReference предоставляют более гибкие и эффективные способы высвобождения ресурсов, когда объект становится недоступным.   -  person Elliott Frisch    schedule 18.10.2018
comment
JFYI в Java 9, в отличие от Java 8, мы должны использовать PhantomReference для очистки post-mortem. Поэтому, если мы нашли экземпляр PhantomReference в очереди, это означает, что память для объекта, на который указывает ссылка, была свободна. В Java 8 нам приходилось явно выполнять метод clean для освобождения памяти.   -  person gstackoverflow    schedule 18.06.2019


Ответы (4)


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

person Holger    schedule 19.10.2018
comment
Спасибо за этот отличный ответ! И все же, при всем уважении, я думаю, что мы в основном согласны с тем, что 1) Cleaner/PhantomReference — это просто оптимизированная версия finalize (и существует лемма, что оптимизированный нет автоматически означает лучше.) 2) Устаревание finalize было актом беспокойства и отчаяния против n00bs. На самом деле это никуда не денется и не менее правильно, чем Cleaner/PhantomReference. - person Aleksandr Dubinsky; 19.10.2018
comment
И не поймите меня неправильно, после изучения Netty я открыл для себя полезность эффективного и гибкого альтернативного решения для финализации. У Netty есть система ручного управления памятью с подсчетом ссылок поверх собственной памяти, и она использует WeakReferences (как ни странно, не PhantomReferences) в необязательных и выборочных стратегиях обнаружения утечек памяти. Использование finalize было бы слишком медленным. Тем не менее, это редкий случай, и рекомендуется избегать ненужной оптимизации. - person Aleksandr Dubinsky; 19.10.2018
comment
Ну, finalize() никогда не должно было существовать. И никогда не поздно признать, что что-то было плохой идеей. Что касается WeakReference против PhantomReference, я также использовал WeakReference, когда мне это действительно было нужно. Как было сказано, до Java 9 фантомные ссылки не очищались автоматически, что требовало сбора референта во втором цикле, что делало его менее эффективным. Кроме того, единственное отличие состоит в том, что объект должен быть завершен до того, как фантомная ссылка будет поставлена ​​в очередь, поэтому, когда вы не используете финализаторы, разницы больше нет. - person Holger; 19.10.2018
comment
Вздох, а может, это все плохие идеи. Вместо этого должен существовать встроенный оптимизированный механизм специального назначения для отладки утечек ресурсов, аналогичный ResourceLeakDetector Netty, за которым следует ожидание того, что утечек не существует. Тогда нам вообще не понадобятся финализаторы. Или еще лучше, мы могли бы иметь встроенный автоматический подсчет ссылок. Теперь это было бы полезно. - person Aleksandr Dubinsky; 19.10.2018
comment
Cleaner кажется довольно плохой заменой сети безопасности, которую предоставил finalize, требуя потока для каждого типа объекта, которому нужна такая система безопасности. По крайней мере, стандартная реализация могла бы предоставить общий Cleaner до того, как приложения Java любого размера начнут иметь десятки ненужных потоков, висящих вокруг для очистки экземпляров одного класса. - person john16384; 10.05.2020
comment
@ john16384 один Cleaner можно использовать для разных типов объектов. Фреймворки могут свободно определять свои общие Cleaner; это не отличается от ExecutorService. Но если действия по очистке совершенно несвязанных ресурсов зависят друг от друга (длительное выполнение действия по очистке может задержать другие действия по очистке), вы сталкиваетесь с проблемами finalize(). Но чаще всего ошибка начинается с мысли, что такая подстраховка нужна для конкретного приложения. - person Holger; 10.05.2020

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

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

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

person Naman    schedule 18.10.2018
comment
Я просто скопирую и вставлю свой комментарий Эллиоту: Ну, я не согласен с тем, что финализация может иметь все перечисленные проблемы, но я не понимаю, почему Cleaner более гибок и эффективен. Проблемы с производительностью? Проверять. Взаимоблокировки/зависания? Почему нет. Ошибки, вызывающие утечку ресурсов? Конечно. Возможность отмены? Я не вижу никакого Cleanable.cancel. Гарантии заказа? Вовсе нет. Гарантия сроков? Забудь об этом. Серьезно, что они нас пытаются продать? - person Aleksandr Dubinsky; 18.10.2018
comment
Не очень хорошо знаком с тоном, но это одна из причин, чтобы исключить его из связанных документов для чтения mindprod .com/jgloss/finalize.html - person Naman; 18.10.2018
comment
@AleksandrDubinsky Cleanable.clean(): Отменяет регистрацию объекта очистки и вызывает действие очистки. Это то, что вы должны вызывать, например в методе close() вашего класса ресурсов. Затем GC нужен только в том случае, если вызов close() был забыт. Но при корректном закрытии вы получаете своевременную очистку и отмену регистрации вне зависимости от того, когда произойдет следующая сборка мусора. - person Holger; 18.10.2018
comment
@Holger Да, я заметил это, читая код, и включил его в свой ответ. clean() вызывает Reference.clear(), что предотвращает постановку ссылки в ReferenceQueue, а также удаляет ссылки на нее, чтобы ее можно было собрать при первом проходе. Finalizer может предложить ту же функцию, поскольку она также наследует Reference, за исключением того факта, что к ней нет доступа. - person Aleksandr Dubinsky; 19.10.2018
comment
@AleksandrDubinsky тот факт, что finalize() реализован с помощью специального объекта Reference, является деталью реализации. Но хуже, чем невозможность отказаться, является отсутствие контроля над регистрацией. Объект класса с нетривиальным методом finalize() регистрируется при выполнении конструктора Object() до выделения ресурса, когда объект даже не инициализирован. Существует так называемая атака финализатора, позволяющая заполучить неинициализированный или недопустимый объект, даже если конструктор выдал исключение. - person Holger; 19.10.2018
comment
Может ли кто-нибудь объяснить, как использовать PhantomReference и PhantomReference + Cleaner? - person gstackoverflow; 18.06.2019

Не используйте ни то, ни другое.

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

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

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

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

person Aleksandr Dubinsky    schedule 21.10.2018
comment
Утечка - это другое. У вас могут быть утечки памяти, но никто не утверждает, что сборщик мусора должен их исправлять. Память — это такой же ресурс, как и любой другой, который желательно высвобождать как можно скорее. Для ресурса памяти есть стандартный механизм. Этот механизм должен быть доступен и для других типов ресурсов. Будут ситуации, когда вы захотите высвободить такие ресурсы как можно быстрее, но есть гораздо больше случаев, когда в конечном итоге все в порядке, как это уже видно во многих приложениях, которые не закрывают сокеты, файлы и т. Д., И все же умудряются нормально работать благодаря доработка. - person john16384; 10.05.2020
comment
@ john16384 Вы читали часть о преждевременной финализации? Кроме того, вы действительно не закрываете свои файлы? Ваш комментарий настолько запутан, что я шокирован тем, что вы называете себя специалистом по Java. - person Aleksandr Dubinsky; 10.05.2020

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

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

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

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

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

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

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

ResourceLeakDetector от Netty гораздо круче, чем Cleaner.

person Aleksandr Dubinsky    schedule 18.10.2018