Мотивация:
Я использую некоторые собственные библиотеки в своем приложении для Android, и я хочу выгрузить их из памяти в определенный момент времени. Библиотеки выгружаются, когда ClassLoader загружает класс, который загружает собственные библиотеки, собирает мусор. Вдохновение: встроенная разгрузка.
Проблема:
- ClassLoader не собирает мусор, если он используется для загрузки некоторого класса (вызывает возможную утечку памяти).
- Нативные библиотеки могут быть загружены только в один ClassLoader в приложении. Если в памяти еще есть старый ClassLoader, и новый ClassLoader пытается загрузить одни и те же собственные библиотеки в какой-то момент времени, генерируется исключение.
Вопрос:
- Как выполнить разгрузку родной библиотеки в чистом виде (разгрузка - моя конечная цель, независимо от того, является ли она плохой техникой программирования или что-то в этом роде).
- Почему возникает утечка памяти и как ее избежать?
В приведенном ниже коде я упрощаю случай, опуская код загрузки исходной библиотеки, как раз демонстрируется утечка памяти Classloader.
Я тестирую это на Android KitKat 4.4.2, API 19. Устройство: Motorola Moto G.
Для демонстрации у меня есть следующий ClassLoader, полученный из PathClassLoader
, используемый для загрузки приложений для 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();
}
}
У меня есть EmptyClass
для загрузки с помощью LibClassLoader
.
package com.demo;
public class EmptyClass {
}
И утечка памяти возникает в следующем коде:
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...
Важно отметить, что родительский элемент cl2
не является ctxt.getClassLoader()
, загрузчиком классов, загружающим класс демонстрационного кода. Это по дизайну, потому что мы не хотим, чтобы cl2
использовал его parent для загрузки EmptyClass
.
Дело в том, что если memoryLeak==false
, то cl2
получает собранный мусор. Если memoryLeak==true
, появляется утечка памяти. Это поведение не согласуется с наблюдениями на стандартном JVM (я использовал загрузчик классов из [1] для имитации такого же поведения). На JVM в обоих случаях cl2
получает собранный мусор.
Я также проанализировал файл дампа кучи с Eclipse MAT, cl2
не был собран мусором, потому что класс EmptyClass
все еще содержит ссылку на него (поскольку классы содержат ссылки на свои загрузчики классов). Это имеет смысл. Но EmptyClass
не был мусором, собранным без причины, видимо. Путь к корню GC - это просто EmptyClass
. Мне не удалось убедить GC завершить работу EmptyClass
.
Файл HeapDump для memoryLeak==true
можно найти здесь, проект Eclipse Android с демонстрационным приложением для этой утечки памяти здесь.
Я попробовал еще несколько вариантов загрузки EmptyClass
в LibClassLoader
, а именно Class.forName(...)
или cl2.findClass()
. С/без статической инициализации результат всегда был одинаковым.
Я проверил множество онлайн-ресурсов, насколько я знаю, нет статических полей кеширования. Я проверил исходные коды PathClassLoader
и это родительские классы, и я не нашел ничего проблемного.
Я был бы очень благодарен за понимание и любую помощь.
Отказ от ответственности:
- Я согласен, что это не лучший способ сделать что-то, если есть какой-либо лучший вариант, как выгрузить собственную библиотеку, я был бы более чем счастлив использовать эту опцию.
- Я согласен с тем, что в целом я не могу полагаться на GC, который вызывается в течение некоторого временного окна. Даже вызов
System.gc()
- это всего лишь намек на выполнение GC для JVM/Dalvik. Мне просто интересно, почему происходит утечка памяти.
Редактировать 11/11/2015
Чтобы сделать это более понятным, как писал Эрик Хеллман, я говорю о загрузке NDK скомпилированной библиотеки C/С++, динамически связанной, с суффиксом .so.