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:

  1. 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í).
  2. ¿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.

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.

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.

  • Variables que se recolectan basura
  • Monodroid: Realización de un GC completo
  • Gran cantidad de memoria no recogida de basura
  • Referencia débil para la llamada de red mala idea?
  • LeakCanary detecta pérdida de memoria de Android WebView
  • Lona: intentando usar un mapa de bits reciclado android.graphics.Bitmap
  • Creación de perfiles y optimización de un juego Android
  • Diferencia entre system.gc () y runtime.gc ()
  • Cómo escuchar eventos de GC en Android
  • Android - Bitmap y gestión de la memoria?
  • Cómo explícitamente realizar la recolección de basura
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.