Утечка памяти класса ClassLoader
Мотивация:
Я использую некоторые собственные библиотеки в своем приложении для 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.
Ответы
Ответ 1
Сначала давайте рассмотрим здесь терминологию.
Является ли это родной библиотекой с привязками JNI, которую вы хотите загрузить? То есть файл с суффиксом .so, который реализован в C/С++ с помощью Android NDK? Обычно это то, о чем мы говорим, когда говорим о родной библиотеке. Если это так, то единственный способ решить это - запустить библиотеку в отдельном процессе. Самый простой способ сделать это - создать службу Android, где вы добавите android:process=":myNativeLibProcess"
для записи в манифесте. Эта Служба затем вызовет System.loadLibrary()
, как обычно, и вы привязываетесь к Сервису из основного процесса, используя Context.bindService()
.
Если это набор классов Java внутри JAR файла, мы смотрим на что-то еще. Для Android вам необходимо создать компиляцию кода вашей библиотеки в файл DEX, который помещается в файл JAR и загружается с помощью DexClassLoader
, аналогично тому, что вы делали в коде.
Когда вы хотите выгрузить библиотеку, вам нужно освободить все ссылки на созданные вами экземпляры и загрузчик классов, используемый для загрузки библиотеки. Это позволит вам позже загрузить новую версию библиотеки. Единственная проблема заключается в том, что вы не будете восстанавливать всю память, используемую незагруженной библиотекой, на устройствах с уровнем API 19 и ниже (например, версии Android с использованием Dalvik VM), поскольку определения классов не собирают мусор. Для Lollipop и позже новая виртуальная машина также будет мусором собирать определения классов, поэтому для этих устройств это будет работать лучше.
Надеюсь, что это поможет.
Ответ 2
Возможно, вы найдете ответы здесь
Я не уверен, что это вы ищете, но он дает фактический метод деаллокации библиотеки в JVM.