Fuga de memoria de Android ClassLoader
Motivación:
Estoy usando algunas bibliotecas nativas en mi aplicación de Android y quiero descargarlas de la memoria en algún momento. Las bibliotecas se descargan cuando ClassLoader que carga la clase que carga bibliotecas nativas obtiene la basura recopilada. Inspiración: descarga nativa .
Problema:
- ClassLoader no es recolección de basura si se utiliza para cargar alguna clase (causa una posible pérdida de memoria).
- Las bibliotecas nativas sólo se pueden cargar en un ClassLoader en la aplicación. Si todavía hay un antiguo ClassLoader colgado en algún lugar de la memoria y un nuevo ClassLoader intenta cargar las mismas librerías nativas en algún momento, se genera una excepción.
Pregunta:
- Cómo realizar la descarga de una biblioteca nativa de una manera limpia (descargando es mi objetivo final, no importa si es una mala técnica de programación o algo así).
- ¿Por qué aparece la pérdida de memoria y cómo evitarla?
En el código abajo simplifico el caso omitiendo un código de carga de biblioteca nativa, sólo se demuestra la pérdida de memoria de Classloader.
- 17.8 MiB asignación de montón para un simple "Hello World" proyecto?
- Determinar cuándo se ejecuta el Android GC
- ¿Cómo puedo evitar los retrasos de recolección de basura en juegos Java? (Mejores Prácticas)
- ¿Cómo solucionar GC_concurrent liberado?
- ¿Es la agrupación de objetos pequeños más eficiente que el recolector de basura Java de Android?
Estoy probando esto en el KitKat Android 4.4.2, API 19. Dispositivo: Motorola Moto G.
Para la demostración tengo el siguiente ClassLoader, derivado de PathClassLoader
utilizado para cargar aplicaciones de Android.
package com.demo; import android.util.Log; import dalvik.system.PathClassLoader; public class LibClassLoader extends PathClassLoader { private static final String THIS_FILE="LibClassLoader"; public LibClassLoader(String dexPath, String libraryPath, ClassLoader parent) { super(dexPath, libraryPath, parent); } @Override protected void finalize() throws Throwable { Log.v(THIS_FILE, "Finalizing classloader " + this); super.finalize(); } }
Tengo EmptyClass
para cargar con el LibClassLoader
.
package com.demo; public class EmptyClass { }
Y la pérdida de memoria se produce en el código siguiente:
final Context ctxt = this.getApplicationContext(); PackageInfo pinfo = ctxt.getPackageManager().getPackageInfo(ctxt.getPackageName(), 0); LibClassLoader cl2 = new LibClassLoader( pinfo.applicationInfo.publicSourceDir, pinfo.applicationInfo.nativeLibraryDir, ClassLoader.getSystemClassLoader()); // Important: parent cannot load EmptyClass. if (memoryLeak){ Class<?> eCls = cl2.loadClass(EmptyClass.class.getName()); Log.v("Demo", "EmptyClass loaded: " + eCls); eCls=null; } cl2=null; // Try to invoke GC System.runFinalization(); System.gc(); Thread.sleep(250); System.runFinalization(); System.gc(); Thread.sleep(500); System.runFinalization(); System.gc(); Debug.dumpHprofData("/mnt/sdcard/hprof"); // Dump heap, hardcoded path...
Lo importante a tener en cuenta es que el padre del cl2
no es ctxt.getClassLoader()
, el cargador de clases que cargó la clase de código de demostración. Esto es por diseño porque no queremos que cl2
use su padre para cargar el EmptyClass
.
La cosa es que si memoryLeak==false
, entonces cl2
obtiene la basura recogida. Si memoryLeak==true
, aparece una pérdida de memoria. Este comportamiento no es coherente con las observaciones en la JVM estándar (he utilizado el cargador de clase de [ 1 ] para simular el mismo comportamiento). En JVM en ambos casos cl2
se recoge basura.
También he analizado el archivo de volcado de pila con Eclipse MAT, cl2
no fue recolectado de basura porque la clase EmptyClass
todavía tiene referencia en él (como las clases tienen referencias en sus cargadores de clase). Esto tiene sentido. Pero EmptyClass
no era basura recogida de ninguna razón, aparentemente. Ruta de EmptyClass
a la raíz de GC es sólo esto EmptyClass
. No he logrado persuadir GC para finalizar EmptyClass
.
Archivo HeapDump para memoryLeak==true
se puede encontrar aquí , Eclipse proyecto Android con una aplicación de demostración para esta fuga de memoria aquí .
También intenté otras variaciones de cargar el EmptyClass
en el LibClassLoader
, es decir, Class.forName(...)
o cl2.findClass()
. Con / sin inicialización estática, el resultado fue siempre el mismo.
He comprobado una gran cantidad de recursos en línea, no hay campos estáticos de almacenamiento en caché involucrados, por lo que sé. Comprobé los códigos fuente del PathClassLoader
y sus clases de padres y no encontré nada problemático.
Estaría muy agradecido por las ideas y cualquier ayuda.
Renuncia:
- Acepto que esta no es la mejor manera de hacer las cosas, si hay alguna mejor opción de cómo descargar una biblioteca nativa, estaría más que feliz de usar esa opción.
- Acepto que en general no puedo confiar en que el GC sea invocado en algún momento. Incluso llamar a
System.gc()
es sólo una sugerencia para ejecutar GC para la JVM / Dalvik. Sólo me pregunto por qué hay una fuga de memoria.
Editar 11/11/2015
Para que sea más claro como Erik Hellman escribió, estoy hablando de la carga NDK compilado biblioteca C / C ++, vinculado dinámicamente, con el sufijo .so.
- ¿Cómo encontrar el origen de un GC_FOR_MALLOC?
- GC_FOR_ALLOC es más "serio" al investigar el uso de la memoria?
- Garbage Collection causa: MediaPlayer finalizado sin ser lanzado
- Obtener la lista de procesos en ejecución y eliminar sus servicios de fondo
- Multithreading de Android: WaitForGcToComplete después de enviar la aplicación al fondo
- En una aplicación React Native JavaScript, ¿por qué cambiaría el comportamiento de Android GC si creaba una variable temporal en lugar de devolver un valor directamente?
- Recogida de basura GridView de Android (GC_EXTERNAL_ALLOC) <1K excesivamente, causando una interfaz de usuario muy intermitente
- Android adecuada limpieza / eliminación
En primer lugar, vamos a resolver la terminología aquí.
¿Es una biblioteca nativa con enlaces JNI que desea cargar? Es decir, un archivo con el sufijo .so que se implementa en C / C ++ utilizando el Android NDK? Normalmente es lo que nos referimos cuando hablamos de biblioteca nativa. Si este es el caso, entonces la única manera de resolver esto es ejecutar la biblioteca en un proceso separado. La forma más sencilla de hacerlo es crear un servicio de Android donde agregue android:process=":myNativeLibProcess"
para la entrada en el manifiesto. Este servicio llamará System.loadLibrary()
como de costumbre y se enlaza al servicio desde su proceso principal utilizando Context.bindService()
.
Si se trata de un conjunto de clases Java dentro de un archivo JAR, entonces estamos buscando algo más. Para Android, debes crear compila tu código de biblioteca en un archivo DEX que se coloca en un archivo JAR y se carga con un DexClassLoader
, similar a lo que has hecho en tu código. Cuando desee descargar la biblioteca, debe liberar todas las referencias a las instancias que ha creado Y el cargador de clases utilizado para cargar la biblioteca. Esto le permitirá cargar una nueva versión de la biblioteca más adelante. El único problema con esto es que no recuperará toda la memoria utilizada por la biblioteca descargada en los dispositivos con nivel API 19 y inferior (es decir, versiones de Android con Dalvik VM) porque las definiciones de clase no se recolectan basura. Para Lollipop y más tarde, la nueva VM también recogerá las definiciones de clase, por lo que para estos dispositivos funcionará mejor.
Espero que ayude.
Puede ser que usted puede encontrar respuestas aquí
No estoy seguro de que es esto que está buscando, pero que da la biblioteca real deallocating método en JVM.
- ¿Cuándo se llama a View.onMeasure ()?
- ¿Es posible conectar la API de Google Maps a través de proxy inverso en mi aplicación?