r17b - ¿Debería evitarse realmente el finalizador Java también para la gestión del ciclo de vida de los objetos pares nativos?
ndk r13 (6)
En mi experiencia como desarrollador de C ++ / Java / Android, he aprendido que los finalizadores son casi siempre una mala idea, con la única excepción de la administración de un objeto "nativo de pares" que necesita Java para llamar al código C / C ++ a través de JNI.
Conozco el JNI: administra adecuadamente la vida útil de una pregunta de objeto java , pero esta pregunta aborda las razones para no usar un finalizador de todos modos, ni para los pares nativos . Entonces, es una pregunta / discusión sobre una confutación de las respuestas en la pregunta antes mencionada.
Joshua Bloch en su Effective Java explícitamente enumera este caso como una excepción a su famoso consejo sobre no usar finalizadores:
Un segundo uso legítimo de finalizadores se refiere a objetos con pares nativos. Un par nativo es un objeto nativo al que un objeto normal delega a través de métodos nativos. Debido a que un par nativo no es un objeto normal, el recolector de basura no lo conoce y no puede reclamarlo cuando se reclama su par de Java. Un finalizador es un vehículo apropiado para realizar esta tarea, suponiendo que el par nativo no tenga recursos críticos. Si el par nativo contiene recursos que deben terminarse rápidamente, la clase debe tener un método de terminación explícito, como se describe anteriormente. El método de terminación debe hacer lo que sea necesario para liberar el recurso crítico. El método de terminación puede ser un método nativo o puede invocar uno.
(Consulte también "¿Por qué se incluye el método finalizado en Java?" Pregunta en stackexchange)
Luego vi la muy interesante Cómo administrar la memoria nativa en la conversación de Android en Google I / O ''17, donde Hans Boehm aboga contra el uso de finalizadores para administrar pares nativos de un objeto Java , y cita Java efectivo como referencia. Después de mencionar rápidamente por qué la eliminación explícita del par nativo o el cierre automático basado en el alcance puede no ser una alternativa viable, aconseja utilizar java.lang.ref.PhantomReference
lugar.
Él hace algunos puntos interesantes, pero no estoy completamente convencido. Trataré de revisar algunos de ellos y expresar mis dudas, esperando que alguien pueda arrojar más luz sobre ellos.
A partir de este ejemplo:
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);
}
}
Cuando una clase Java contiene una referencia a un par nativo que se elimina en el método del finalizador, Bloch enumera las deficiencias de dicho enfoque.
Los finalizadores pueden ejecutarse en orden arbitrario
Si dos objetos se vuelven inalcanzables, los finalizadores realmente se ejecutan en un orden arbitrario, que incluye el caso cuando dos objetos que apuntan el uno al otro se vuelven inalcanzables al mismo tiempo que se pueden finalizar en el orden incorrecto, lo que significa que el segundo se finalizó intenta acceder a un objeto que ya ha sido finalizado. [...] Como resultado, puede obtener punteros colgantes y ver objetos C ++ desasignados [...]
Y como un ejemplo:
class SomeClass {
BinaryPoly mMyBinaryPoly:
…
// DEFINITELY DON’T DO THIS WITH CURRENT BinaryPoly!
protected void finalize() {
Log.v(“BPC”, “Dropped + … + myBinaryPoly.toString());
}
}
Bien, pero ¿no es cierto también si myBinaryPoly es un objeto puro de Java? Según tengo entendido, el problema proviene de operar en un objeto posiblemente finalizado dentro del finalizador de su propietario. En caso de que solo usemos el finalizador de un objeto para eliminar su propio par nativo privado y no hagamos nada más, estaríamos bien, ¿no?
El finalizador puede invocarse mientras el método nativo está funcionando
Por reglas de Java, pero actualmente no en Android:
El finalizador de Object x puede invocarse mientras uno de los métodos de x aún se está ejecutando, y acceder al objeto nativo.
El pseudocódigo de lo que se compila multiply()
se muestra para explicar esto:
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;
}
Esto es aterrador, y en realidad estoy aliviado de que esto no ocurra en Android, porque lo que entiendo es que this
y other
son recolectados antes de que salgan del alcance. Esto es aún más extraño si se tiene en cuenta que this
es el objeto al que se recurre, y que el other
es el argumento del método, por lo que ambos deberían estar "vivos" en el ámbito al que se llama el método.
Una solución rápida a esto sería llamar a algunos métodos ficticios en this
y en other
(¡feo!), O pasarlos al método nativo (donde podemos recuperar el mNativeHandle
y operarlo). Y espera ... ¡ this
ya es por defecto uno de los argumentos del método nativo!
JNIEXPORT void JNICALL Java_package_BinaryPoly_multiply
(JNIEnv* env, jobject thiz, jlong xPtr, jlong yPtr) {}
¿Cómo puede ser this
posiblemente basura recogida?
Los finalizadores pueden diferirse por mucho tiempo
"Para que esto funcione correctamente, si ejecuta una aplicación que asigna mucha memoria nativa y relativamente poca memoria java, puede que en realidad no sea el caso de que el recolector de basura se ejecute con la prontitud suficiente para invocar finalizadores [...] de modo que realmente pueda tiene que invocar ocasionalmente System.gc () y System.runFinalization (), lo que es complicado de hacer [...] "
Si el par nativo solo es visto por un único objeto java al que está vinculado, este hecho no es transparente para el resto del sistema, y por lo tanto, el GC debería tener que administrar el ciclo de vida del objeto Java, ya que era un ¿java puro? Claramente hay algo que no veo aquí.
Los finalizadores en realidad pueden extender la vida útil del objeto java
[...] A veces los finalizadores en realidad extienden la vida útil del objeto java para otro ciclo de recolección de basura, lo que significa que para los recolectores de basura generacionales pueden hacer que sobreviva en la generación anterior y la vida útil puede extenderse mucho como resultado de solo tener un finalizador.
Admito que realmente no entiendo cuál es el problema aquí y cómo se relaciona con tener un compañero nativo, investigaré un poco y posiblemente actualizaré la pregunta :)
En conclusión
Por ahora, sigo creyendo que usar un tipo de enfoque RAII era que el par nativo creado en el constructor del objeto java y eliminado en el método finalize no es realmente peligroso, siempre que:
- el par nativo no posee ningún recurso crítico (en ese caso debe haber un método separado para liberar el recurso, el par nativo solo debe actuar como el objeto "contraparte" del objeto Java en el dominio nativo)
- el par nativo no abarca hilos o hace cosas concurrentes extrañas en su destructor (¿quién querría hacer eso?!?)
- el puntero de par nativo nunca se comparte fuera del objeto java, solo pertenece a una única instancia y solo se accede a él dentro de los métodos del objeto java. En Android, un objeto java puede acceder al par nativo de otra instancia de la misma clase, justo antes de llamar a un método jni aceptando diferentes pares nativos o, mejor, simplemente pasando los objetos java al método nativo mismo
- el finalizador del objeto java solo borra su propio par nativo, y no hace nada más
¿Hay alguna otra restricción que deba agregarse, o realmente no hay forma de garantizar que un finalizador sea seguro incluso si se respetan todas las restricciones?
¿Cómo puede ser esto posiblemente basura recogida?
Porque la función nativeMultiply(long xCppPtr, long yCppPtr)
es estática. Si una función nativa es estática, su segundo parámetro es jclass
apuntando a su clase en lugar de a jobject
apuntando a this
. Entonces, en este caso, this
no es uno de los argumentos.
Si no hubiera sido estático, solo habría problemas con el other
objeto.
Creo que la mayor parte de este debate proviene del estado heredado de finalize (). Se introdujo en Java para tratar cosas que la recolección de basura no cubría, pero no necesariamente cosas como los recursos del sistema (archivos, conexiones de red, etc.) por lo que siempre me sentí como medio cocido. No necesariamente estoy de acuerdo con el uso de algo así como phantomreference, que afirma ser un finalizador mejor que finalizar () cuando el patrón en sí es problemático.
Hugues Moreau señaló que finalize () se desaprobará en Java 9. El patrón preferido del equipo de Java parece ser tratar cosas como pares nativos como un recurso del sistema y limpiarlos a través de try-with-resources. AutoCloseable implementación de AutoCloseable permite hacer esto. Tenga en cuenta que try-with-resources y AutoCloseable post-date implican tanto a Josh Bloch directamente con Java como a Effective Java 2nd edition.
Déjame proponer una propuesta provocativa. Si su lado de C ++ de un objeto Java gestionado se puede asignar en la memoria contigua, en lugar del puntero nativo largo tradicional, puede utilizar un DirectByteBuffer . Esto realmente puede ser un cambio de juego: ahora GC puede ser lo suficientemente inteligente acerca de estas pequeñas envolturas de Java alrededor de enormes estructuras de datos nativas (por ejemplo, decidir recogerlo antes).
Desafortunadamente, la mayoría de los objetos C ++ de la vida real no entran en esta categoría ...
Mi propia opinión es que uno debería lanzar objetos nativos tan pronto como haya terminado con ellos, de una manera determinista. Como tal, es preferible utilizar el alcance para gestionarlos que confiar en el finalizador. Puede utilizar el finalizador para realizar la limpieza como último recurso, pero no lo utilizaría únicamente para administrar la vida real por las razones que señaló en su propia pregunta.
Como tal, deje que el finalizador sea el último intento, pero no el primero.
vea https://github.com/android/platform_frameworks_base/blob/master/graphics/java/android/graphics/Bitmap.java#L135 use phantomreference en lugar de finalizer
finalize
y otros enfoques que utilizan el conocimiento de GC de los objetos de por vida tienen un par de matices:
- visibilidad : ¿garantiza que todos los métodos de escritura del objeto o sean visibles para el finalizador (es decir, hay una relación de pase previo entre la última acción en el objeto oy el código que realiza la finalización)?
- accesibilidad : ¿cómo se garantiza que un objeto o no se destruya prematuramente (p. ej., mientras uno de sus métodos se está ejecutando), lo cual está permitido por el JLS? happen y causa bloqueos.
- orden : ¿puede hacer cumplir un cierto orden en el que los objetos se finalizan?
- finalización : ¿necesita destruir todos los objetos cuando finaliza su aplicación?
Es posible resolver todos estos problemas con finalizadores, pero requiere una cantidad decente de código. Hans-J. Boehm tiene una gran presentación que muestra estos problemas y posibles soluciones.
Para garantizar la visibilidad , debe sincronizar su código, es decir, poner operaciones con semántica de publicación en sus métodos habituales y una operación con semántica de adquisición en el finalizador. Por ejemplo:
- Una tienda en un
volatile
al final de cada método + lectura del mismovolatile
en un finalizador. - Libere el bloqueo en el objeto al final de cada método + adquiera el bloqueo al comienzo de un finalizador (consulte la implementación de
keepAlive
en las diapositivas de Boehm).
Para garantizar la accesibilidad (cuando aún no está garantizado por la especificación del idioma), puede usar:
- Sincronización.
-
Reference#reachabilityFence
from Java 9. - Pase referencias a los objetos que deben permanecer alcanzables (= non-finalizable ) en métodos nativos. En la conversación a la que hace referencia ,
nativeMultiply
esstatic
, porthis
tanto, puede ser basura.
La diferencia entre plain finalize
y PhantomReferences
es que este último le da mucho más control sobre los diversos aspectos de la finalización:
- Puede tener varias colas que reciben referencias fantasmas y elegir un hilo que realiza la finalización de cada una de ellas.
- Puede finalizar en el mismo hilo que hizo la asignación (p. Ej., Enhebrar
ReferenceQueues
locales). - Más fácil de hacer cumplir ordenamiento: mantenga una referencia fuerte a un objeto
B
que debe permanecer vivo cuandoA
se finaliza como un campo dePhantomReference
aA
; - Más fácil de implementar la terminación segura, ya que debe mantener
PhantomRefereces
fuertemente accesible hasta que seanPhantomRefereces
por GC.