Должен ли Java-финализатор действительно избегать также для управления жизненным циклом собственных одноранговых объектов?

По моему опыту разработчика C ++ / Java / Android, я пришел к выводу, что финализаторы почти всегда являются плохими идеями, единственным исключением является управление «родным одноранговым» объектом, необходимым java для вызова кода C / C ++ Через JNI.

Я знаю JNI: Правильно управляйте временем жизни объекта Java-объекта , но в этом вопросе рассматриваются причины не использовать финализатор в любом случае, ни для обычных аналогов . Итак, это вопрос / дискуссия о том, как отвечать на ответы в вышеупомянутом вопросе.

Джошуа Блох в своей эффективной Java явно перечисляет это дело как исключение из своего знаменитого совета по поводу не использования финализаторов:

Второе законное использование финализаторов относится к объектам с родными аналогами. Нативный одноранговый узел является нативным объектом, которому обычный объект делегирует через собственные методы. Поскольку собственный аналог не является нормальным объектом, сборщик мусора не знает об этом и не может его вернуть, когда его сверстник Java будет восстановлен. Финализатор является подходящим средством для выполнения этой задачи, предполагая, что у обычного партнера нет критических ресурсов. Если собственный одноранговый узел содержит ресурсы, которые должны быть немедленно завершены, класс должен иметь явный метод завершения, как описано выше. Метод завершения должен делать все, что требуется для освобождения критического ресурса. Метод завершения может быть нативным методом или может вызвать его.

(Также см. «Почему включен завершенный метод, включенный в Java?» Вопрос о stackexchange)

Затем я посмотрел действительно интересное. Как управлять собственной памятью в Android- сообществе в Google I / O '17, где Ханс Бем фактически выступает против использования финализаторов для управления родными одноранговыми узлами Java-объекта , также ссылаясь на Эффективную Java в качестве ссылки. После быстрого упоминания, почему явное удаление собственного однорангового узла или автоматическое закрытие на основе области видимости не может быть жизнеспособной альтернативой, он советует вместо этого использовать java.lang.ref.PhantomReference .

Он делает несколько интересных моментов, но я не совсем уверен. Я попытаюсь пробежать некоторые из них и выразить свои сомнения, надеясь, что кто-то сможет пролить свет на них.

Начиная с этого примера:

 class BinaryPoly { long mNativeHandle; // holds a c++ raw pointer private BinaryPoly(long nativeHandle) { mNativeHandle = nativeHandle; } private static native long nativeMultiply(long xCppPtr, long yCppPtr); BinaryPoly multiply(BinaryPoly other) { return new BinaryPoly ( nativeMultiply(mNativeHandle, other.mNativeHandler) ); } // … static native void nativeDelete (long cppPtr); protected void finalize() { nativeDelete(mNativeHandle); } } 

Если класс java содержит ссылку на собственный одноранговый узел, который удаляется в методе finalizer, Блох перечисляет недостатки такого подхода.

Финализаторы могут работать в произвольном порядке

Если два объекта становятся недоступными, финализаторы фактически выполняются в произвольном порядке, что включает случай, когда два объекта, которые указывают друг на друга, становятся недоступными, в то же время они могут быть финализированы в неправильном порядке, что означает, что второй, который должен быть завершен на самом деле Пытается получить доступ к объекту, который уже был финализирован. […] В результате вы можете обманывать указатели и видеть освобожденные объекты c ++ […]

И в качестве примера:

 class SomeClass { BinaryPoly mMyBinaryPoly: … // DEFINITELY DON'T DO THIS WITH CURRENT BinaryPoly! protected void finalize() { Log.v(“BPC”, “Dropped + … + myBinaryPoly.toString()); } } 

Хорошо, но разве это не так, если myBinaryPoly – это чистый объект Java? Насколько я понимаю, проблема связана с работой над возможно завершенным объектом внутри финализатора его владельца. В случае, если мы используем только финализатор объекта, чтобы удалить его собственный собственный собственный одноранговый узел и ничего не делать, мы должны быть в порядке, не так ли?

Финализатор может вызываться, пока собственный метод до запуска

По правилам Java, но не в настоящее время на Android:
Финализатор объекта x может быть вызван, пока один из методов x все еще запущен, и обращается к собственному объекту.

Показано, что multiply() того, с чем компилируется multiply() объясняет это:

 BinaryPoly multiply(BinaryPoly other) { long tmpx = this.mNativeHandle; // last use of “this” long tmpy = other.mNativeHandle; // last use of other BinaryPoly result = new BinaryPoly(); // GC happens here. “this” and “other” can be reclaimed and finalized. // tmpx and tmpy are still neeed. But finalizer can delete tmpx and tmpy here! result.mNativeHandle = nativeMultiply(tmpx, tmpy) return result; } 

Это страшно, и я действительно с облегчением, это не происходит на андроиде, потому что я понимаю, что this и other собирают мусор, прежде чем они выйдут из сферы действия! Это даже более странно, учитывая, что this объект, на который вызван метод, а other – аргумент метода, поэтому оба они должны уже «быть живыми» в области, где вызывается метод.

Быстрое решение этой проблемы заключалось бы в том, чтобы называть некоторые фиктивные методы как this и other (уродливым!) Или передавать их на нативный метод (где мы можем затем восстановить mNativeHandle и работать с ним). И подождите … this уже по умолчанию один из аргументов собственного метода!

 JNIEXPORT void JNICALL Java_package_BinaryPoly_multiply (JNIEnv* env, jobject thiz, jlong xPtr, jlong yPtr) {} 

Как this возможно собрать мусор?

Финализаторы могут быть отложены слишком долго

«Чтобы это работало корректно, если вы запускаете приложение, которое выделяет много встроенной памяти и относительно небольшую java-память, на самом деле не может быть так, что сборщик мусора работает достаточно быстро, чтобы на самом деле вызывать финализаторы […], поэтому вы действительно можете Должны иногда ссылаться на System.gc () и System.runFinalization (), которые сложно сделать […] "

Если собственный одноранговый узел просматривается только одним объектом Java, к которому он привязан, не является ли этот факт прозрачным для остальной части системы, и поэтому GC должен просто управлять жизненным циклом объекта Java, поскольку он был Чистый java один? Там явно что-то я не вижу здесь.

Финализаторы могут фактически продлить время жизни объекта java

[…] Иногда финализаторы на самом деле продлевают срок службы объекта java для другого цикла сбора мусора, что означает, что сборщики мусора для генерации могут фактически вывести его в старое поколение, и срок его жизни может значительно увеличиться в результате просто С финализатором.

Я признаю, что я действительно не понимаю, в чем проблема, и как это связано с наличием собственного партнера, я сделаю некоторые исследования и, возможно, обновить вопрос 🙂

В заключение

На данный момент я по-прежнему полагаю, что использование своего рода подхода RAII состояло в том, что собственный конструктор создавался в конструкторе объекта java и удалялся в методе finalize, фактически не опасен при условии, что:

  • Собственный одноранговый узел не имеет никакого критического ресурса (в этом случае должен быть отдельный способ освобождения ресурса, собственный одноранговый узел должен действовать только как объект Java «аналог» в родной области)
  • Собственный одноранговый узел не охватывает потоки или не делает странных параллельных данных в своем деструкторе (кто хотел бы сделать это?!?)
  • Внутренний указатель-сверстник никогда не делится вне объекта java, он принадлежит только одному экземпляру и доступен только внутри методов Java-объекта. На Android объект java может получить доступ к собственному одноранговому узлу другого экземпляра того же класса, прямо перед тем, как вызвать метод jni, который принимает разные собственные одноранговые узлы или, лучше, просто передает объекты Java самому собственному методу
  • Финализатор объекта java удаляет свой собственный собственный одноранговый узел и ничего не делает

Есть ли какие-либо другие ограничения, которые необходимо добавить, или нет способа гарантировать, что финализатор безопасен даже при соблюдении всех ограничений?

Solutions Collecting From Web of "Должен ли Java-финализатор действительно избегать также для управления жизненным циклом собственных одноранговых объектов?"

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

Таким образом, пусть финализатор будет последней попыткой, но не первой.

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

Хьюг Моро отметил, что finalize () будет устаревать в Java 9. Предпочитаемый образец команды Java, по-видимому, обрабатывает такие вещи, как родные сверстники, как системный ресурс и очищает их через try-with-resources. Реализация AutoCloseable позволяет это сделать. Обратите внимание, что try-with-resources и AutoCloseable после даты и непосредственного участия Джоша Блоха в Java и Effective Java 2nd edition.

См. https://github.com/android/platform_frameworks_base/blob/master/graphics/java/android/graphics/Bitmap.java#L135 использовать фантомную запись вместо финализатора

finalize и другие подходы, которые используют знания GC о жизни объектов, имеют пару нюансов:

  • Видимость : гарантируете ли вы, что все методы записи объекта o видны финализатору (т. Е. Существует связь между последним действием на объекте o и выполняющим завершение кода)?
  • Достижимость : как вы гарантируете, что объект o не будет уничтожен преждевременно (например, хотя один из его методов запущен), что разрешено JLS? Это происходит и вызывает сбои.
  • Заказ : можете ли вы обеспечить выполнение определенного порядка, в котором объекты завершены?
  • Завершение : вам нужно уничтожить все объекты, когда ваше приложение завершается?

Все эти проблемы можно решить с помощью финализаторов, но для этого требуется достойный код. Hans-J. У Boehm есть отличная презентация, которая показывает эти проблемы и возможные решения.

Чтобы гарантировать видимость , вам необходимо синхронизировать свой код, т. Е. Помещать операции с семантикой Release в ваши обычные методы и операцию с семантикой Acquire в вашем финализаторе. Например:

  • Хранилище в volatile в конце каждого метода + читается с той же volatile в финализаторе.
  • Блокировать блокировку объекта в конце каждого метода + получить блокировку в начале финализатора (см. Реализацию keepAlive в слайдах Boehm).

Чтобы гарантировать доступность (когда это еще не гарантировано спецификацией языка), вы можете использовать:

  • Синхронизация.
  • Reference#reachabilityFence от Java 9.
  • Передавайте ссылки на объекты, которые должны оставаться доступными (= незавершенные ) в собственных методах. В разговоре вы ссылаетесь , nativeMultiply static , поэтому this может быть сбор мусора.

Разница между plain finalize и PhantomReferences заключается в том, что последний дает вам больше контроля над различными аспектами финализации:

  • Может иметь несколько очередей, получающих фантомные ссылки, и выбирать поток, выполняющий финализацию для каждого из них.
  • Может завершиться в том же потоке, который выполнял выделение (например, thread local ReferenceQueues ).
  • Легче обеспечить соблюдение порядка: храните сильную ссылку на объект B который должен оставаться в живых, когда A завершен как поле PhantomReference to A ;
  • Легче реализовать безопасное завершение, так как вы должны постоянно поддерживать PhantomRefereces пока они не будут установлены в GC.

Позвольте мне предложить провокационное предложение. Если ваша C ++-сторона управляемого объекта Java может быть выделена в непрерывной памяти, вместо традиционного длинного встроенного указателя вы можете использовать DirectByteBuffer . На самом деле это может быть игровой чейнджер: теперь GC может быть достаточно увлечен этими маленькими оболочками Java вокруг огромных встроенных структур данных (например, решает собрать их раньше).

К сожалению, большинство реальных объектов C ++ не попадают в эту категорию …