По моему опыту разработчика C++/Java/Android, я пришел к выводу, что финализаторы почти всегда являются плохой идеей, единственным исключением является управление нативным одноранговым объектом, необходимым java-объекту для вызова кода C/C++ через JNI. .
Я знаю о JNI: правильно управлять временем жизни объекта Java вопрос, но этот вопрос касается причин, не использовать финализатор в любом случае, ни для нативных одноранговых узлов. Итак, это вопрос/обсуждение опровержения ответов на вышеупомянутый вопрос.
Джошуа Блох в своей Effective Java явно перечисляет этот случай как исключение из его знаменитого совета не использовать финализаторы:
Второе допустимое использование финализаторов касается объектов с нативными одноранговыми узлами. Нативный одноранговый узел — это нативный объект, которому обычный объект делегирует свои полномочия с помощью нативных методов. Поскольку родной одноранговый объект не является обычным объектом, сборщик мусора не знает об этом и не может вернуть его, когда его одноранговый узел Java будет утилизирован. Финализатор является подходящим средством для выполнения этой задачи, при условии, что собственный одноранговый узел не содержит критических ресурсов. Если собственный одноранговый узел содержит ресурсы, которые должны быть немедленно прекращены, класс должен иметь явный метод завершения, как описано выше. Метод завершения должен делать все, что требуется для освобождения критического ресурса. Метод завершения может быть собственным методом или вызывать его.
(Также см. Почему финализированный метод включен в Java? а> вопрос по stackexchange)
Затем я посмотрел действительно интересное выступление Как управлять встроенной памятью в Android в Google I /O '17, где Ганс Бём на самом деле выступает против использования финализаторов для управления собственными одноранговыми объектами Java, также ссылаясь на Effective 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 содержит ссылку на собственный одноранговый класс, который удаляется в методе финализатора, Блох перечисляет недостатки такого подхода.
Финализаторы могут запускаться в произвольном порядке
Если два объекта становятся недостижимыми, финализаторы фактически запускаются в произвольном порядке, включая случай, когда два объекта, которые указывают друг на друга, становятся недостижимыми одновременно, они могут быть финализированы в неправильном порядке, а это означает, что второй объект, подлежащий финализации, на самом деле пытается получить доступ к уже завершенному объекту. [...] В результате вы можете получить оборванные указатели и увидеть освобожденные объекты c++ [...]
И в качестве примера:
class SomeClass {
BinaryPoly mMyBinaryPoly:
…
// DEFINITELY DON’T DO THIS WITH CURRENT BinaryPoly!
protected void finalize() {
Log.v(“BPC”, “Dropped + … + myBinaryPoly.toString());
}
}
Хорошо, но разве это не так, если myBinaryPoly является чистым объектом Java? Насколько я понимаю, проблема возникает из-за работы с возможно финализированным объектом внутри финализатора его владельца. В случае, если мы используем финализатор объекта только для удаления его собственного частного однорангового узла и ничего больше не делаем, все должно быть в порядке, верно?
Finalizer может быть вызван, пока работает собственный метод
По правилам Java, но не в настоящее время для Android:
финализатор объекта x может быть вызван, пока один из методов x все еще работает и обращается к нативному объекту.
Псевдокод того, во что компилируется 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 needed. 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 в родной области)
- родной одноранговый узел не разделяет потоки и не делает странных одновременных действий в своем деструкторе (кто бы захотел это делать?!?)
- родной одноранговый указатель никогда не передается за пределы объекта java, принадлежит только одному экземпляру и доступен только внутри методов объекта java. В Android объект java может получить доступ к собственному узлу другого экземпляра того же класса прямо перед вызовом метода jni, принимающего разные собственные узлы, или, что лучше, просто передать объекты java самому собственному методу.
- финализатор java-объекта удаляет только свой родной одноранговый узел и больше ничего не делает
Есть ли какие-либо другие ограничения, которые следует добавить, или действительно нет способа гарантировать, что финализатор безопасен даже при соблюдении всех ограничений?
dispose()
(или чего-то еще), если управляющий код не может этого сделать - и часто выводит предупреждения журнала. - person chrylis -cautiouslyoptimistic-   schedule 21.05.2017private BinaryPoly(long nativeHandle)
не должно быть, и весь этот пример очень искусственный. - person Alex Cohn   schedule 22.05.2017forRemoval=true
, поэтому на данном этапе нет определенного плана по фактическому его удалению. Это не умаляет достоинства аргументов в пользу использованияfinalize()
, но я думаю, что он заслуживает упоминания (в конце концов, вопрос имеет тегjava
). - person Hugues M.   schedule 04.06.2017