Motivation:
I am using some native libraries in my Android application and I want to unload them from the memory at some point in time. Libraries get unloaded when ClassLoader that loaded class that loaded native libraries gets garbage collected. Inspiration: native unloading.
Problem:
- ClassLoader is not garbage collected if it is used to load some class (causes a possible memory leak).
- Native libraries can be loaded only in one ClassLoader in the application. If there is still old ClassLoader hanging somewhere in the memory and a new ClassLoader tries to load same native libraries at some point in time, exception is thrown.
Question:
- How to perform unloading of a native library in a clean way (unloading is my ultimate target, no matter if it is a poor programming technique or something like that).
- Why the memory leak appears and how to avoid it?
In the code below I simplify the case by omitting a native library loading code, just Classloader memory leak is demonstrated.
I am testing this on the Android KitKat 4.4.2, API 19. Device: Motorola Moto G.
For the demonstration I have the following ClassLoader, derived from PathClassLoader
used for loading Android applications.
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();
}
}
I have the EmptyClass
to load with the LibClassLoader
.
package com.demo;
public class EmptyClass {
}
And the memory leak is caused in the following code:
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...
The important thing to note is that parent of the cl2
is not ctxt.getClassLoader()
, the classloader that loaded the demonstration code class. This is by design because we don't want cl2
to use it's parent to load the EmptyClass
.
The thing is that if memoryLeak==false
, then cl2
gets garbage collected. If memoryLeak==true
, memory leak appears. This behavior is not consistent with observations on standard JVM (I used class loader from [1] to simulate the same behavior). On JVM in both cases cl2
gets garbage collected.
I also analyzed heap dump file with Eclipse MAT, cl2
was not garbage collected because class EmptyClass
still holds reference on it (as classes hold references on their class loaders). This makes sense. But EmptyClass
was not garbage collected from no reason, apparently. Path to GC root is just this EmptyClass
. I haven't managed to persuade GC to finalize EmptyClass
.
HeapDump file for memoryLeak==true
can be found here, Eclipse Android project with a demonstration application for this memory leak here.
I tried also another variations of loading the EmptyClass
in the LibClassLoader
, namely Class.forName(...)
or cl2.findClass()
. With/without static initialization, result was always the same.
I checked a lot of online resources, there are no static caching fields involved, as far as I know. I checked source codes of the PathClassLoader
and it's parent classes and I found nothing problematic.
I would be very thankful for insights and any help.
Disclaimer:
- I accept this is not the best way of doing things, if there is any better option how to unload a native library, I would be more than happy to use that option.
- I accept that in general I cannot rely on the GC to be invoked in some time window. Even calling
System.gc()
is only a hint to execute GC for the JVM/Dalvik. I am just wondering why there is a memory leak.
Edit 11/11/2015
To make it more clear as Erik Hellman wrote, I am speaking about loading NDK compiled C/C++ library, dynamically linked, with .so suffix.