Подтвердить что ты не робот

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

В моем опыте разработчика С++/Java/Android я пришел к выводу, что финализаторы - это почти всегда плохая идея, единственное исключение - это управление "родным равноправным" объектом, требуемым java для вызова 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, Блох перечисляет недостатки такого подхода.

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

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

И как пример:

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:
Объект xs finalizer может быть вызван, когда один из xs-методов все еще запущен, и обращается к собственному объекту.

Ниже показан псевдокод того, что компилируется 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;
}

Это страшно, и я действительно с облегчением, это не происходит на Android, потому что я понимаю, что 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-партнера "counterpart" в нативной сфере)
  • собственный одноранговый узел не охватывает потоки или не делает странных параллельных данных в своем деструкторе (кто хотел бы сделать это?!?)
  • Внутренний указатель-указатель никогда не делится вне объекта java, он принадлежит только одному экземпляру и доступен только внутри методов Java-объектов. На Android объект java может получить доступ к собственному одноранговому узлу другого экземпляра того же класса, прямо перед вызовом jni-метода, принимающего разные собственные одноранговые узлы, или, лучше, просто передавая Java-объекты самому собственному методу
  • финализатор объектов Java удаляет свой собственный собственный одноранговый узел и ничего не делает

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

4b9b3361

Ответ 1

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

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

Ответ 2

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

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

Ответ 3

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

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

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

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

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

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

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

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

Ответ 5

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

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