Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android ClassLoader memory leak

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:

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

like image 849
ph4r05 Avatar asked Jun 15 '14 00:06

ph4r05


People also ask

What is a ClassLoader leak?

Classloader Leaks. Especially in application servers and OSGi containers, there is another form of memory leak: the classloader leak. As classes are referenced by their classloaders, they get removed when the classloader is garbage-collected. That will happen only when the application gets unloaded.

What is ClassLoader in Android?

This class loader is used to load classes and resources from a search path of URLs referring to both JAR files and directories. A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class.

What is ClassLoader in Java stack overflow?

Class Loaders are a functional component of JVM, which loads class data from the '. class' file or from over the network in to the Method Area in Heap.

What is class loader memory?

Class loader memory leakClasses with the same name can be loaded multiple times in a single JVM, each in a different class loader. Each web application gets its own class loader and this is what WebSphere® Application Server uses for isolating applications.


1 Answers

First, lets sort out the terminology here.

Is it a native library with JNI bindings that you want to load? That is, a file with the suffix .so that is implemented in C/C++ using the Android NDK? That is usually what we refer to when we talk about native library. If this is the case, then the only way to solve this is to run the library in separate process. Easiest way to do this is to create an Android service where you add android:process=":myNativeLibProcess" for the entry in the manifest. This Service will then call System.loadLibrary() as usual and you bind to the Service from your main process using Context.bindService().

If it is a set of Java classes inside a JAR file, then we are looking at something else. For Android, you need to create compile your library code into a DEX file that is placed into a JAR file and loaded using a DexClassLoader, similar to what you've done in your code. When you want to unload the library, you need to release all the references to the instances you've created AND the class loader used for loading the library. This will allow you to load a new version of the library later on. The only problem with this is that you won't reclaim all the memory used by the unloaded library on devices with API level 19 and lower (i.e., Android versions using Dalvik VM) because class definitions are not garbage collected. For Lollipop and later, the new VM will also garbage collect class definitions, so for these devices this will work better.

Hope that helps.

like image 61
Erik Hellman Avatar answered Oct 04 '22 15:10

Erik Hellman