В моем опыте разработчика С++/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 удаляет свой собственный собственный одноранговый узел и ничего не делает
Есть ли какие-либо другие ограничения, которые необходимо добавить, или нет способа гарантировать, что финализатор безопасен даже при соблюдении всех ограничений?