Утечка памяти класса ClassLoader

Мотивация:

Я использую некоторые собственные библиотеки в своем приложении для Android, и я хочу выгрузить их из памяти в определенный момент времени. Библиотеки выгружаются, когда ClassLoader загружает класс, который загружает собственные библиотеки, собирает мусор. Вдохновение: естественная разгрузка .

Проблема:

  • ClassLoader не собирает мусор, если он используется для загрузки некоторого класса (вызывает возможную утечку памяти).
  • Нативные библиотеки могут быть загружены только в один ClassLoader в приложении. Если в памяти еще есть старый ClassLoader, и новый ClassLoader пытается загрузить одни и те же родные библиотеки в какой-то момент времени, генерируется исключение.

Вопрос:

  1. Как выполнить разгрузку родной библиотеки по-разному (разгрузка – моя конечная цель, независимо от того, является ли она плохой техникой программирования или что-то в этом роде).
  2. Почему появляется утечка памяти и как ее избежать?

В приведенном ниже коде я упрощаю случай, опуская код загрузки загружаемой библиотеки, демонстрируется только утечка памяти 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 использовал его родительский EmptyClass для загрузки EmptyClass .

Дело в том, что если memoryLeak==false , то cl2 получает сбор мусора. Если memoryLeak==true , появляется утечка памяти. Это поведение не согласуется с наблюдениями на стандартном JVM (я использовал загрузчик классов из [ 1 ], чтобы имитировать такое же поведение). На JVM в обоих случаях cl2 получает сбор мусора.

Я также проанализировал файл дампа кучи с Eclipse MAT, cl2 не был собран мусором, потому что класс EmptyClass все еще содержит ссылку на него (поскольку классы содержат ссылки на свои загрузчики классов). Это имеет смысл. Но, по- EmptyClass не был мусором, собранным без причины. Путь к GC root – это просто EmptyClass . Мне не удалось убедить GC завершить EmptyClass над EmptyClass .

Файл HeapDump для memoryLeak==true можно найти здесь , проект Eclipse Android с демонстрационным приложением для этой утечки памяти здесь .

Я также попробовал другие варианты загрузки EmptyClass в LibClassLoader , а именно Class.forName(...) или cl2.findClass() . С / без статической инициализации результат всегда был одинаковым.

Насколько я знаю, я проверил множество онлайн-ресурсов. Я проверил исходные коды PathClassLoader и родительских классов, и я не нашел ничего проблемного.

Я был бы очень благодарен за понимание и любую помощь.

Отказ от ответственности:

  • Я согласен, что это не лучший способ делать вещи, если есть какой-то лучший вариант, как разгрузить родную библиотеку, я был бы более чем счастлив использовать эту опцию.
  • Я согласен с тем, что в целом я не могу полагаться на GC, который вызывается в течение некоторого временного окна. Даже вызов System.gc() – это всего лишь намек на выполнение GC для JVM / Dalvik. Мне просто интересно, почему происходит утечка памяти.

Редактировать 11/11/2015

Чтобы сделать это более понятным, как писал Эрик Хеллман, я говорю о загрузке NDK скомпилированной библиотеки C / C ++, динамически связанной, с .so-суффиксом.

Solutions Collecting From Web of "Утечка памяти класса ClassLoader"

Во-первых, давайте рассмотрим здесь терминологию.

Является ли это родной библиотекой с привязками JNI, которую вы хотите загрузить? То есть файл с суффиксом .so, который реализован в C / C ++ с помощью Android NDK? Обычно это то, о чем мы говорим, когда говорим о родной библиотеке. Если это так, то единственный способ решить это – запустить библиотеку в отдельном процессе. Самый простой способ сделать это – создать службу Android, где вы добавляете android:process=":myNativeLibProcess" для записи в манифесте. Эта Служба затем вызовет System.loadLibrary() как обычно, и вы привязываетесь к Context.bindService() из основного процесса, используя Context.bindService() .

Если это набор классов Java внутри JAR-файла, мы смотрим на что-то еще. Для Android вам необходимо создать компиляцию кода библиотеки в файл DEX, который помещается в файл JAR и загружается с помощью DexClassLoader , аналогично тому, что вы делали в своем коде. Когда вы хотите выгрузить библиотеку, вам нужно освободить все ссылки на созданные вами экземпляры и загрузчик классов, используемый для загрузки библиотеки. Это позволит вам позже загрузить новую версию библиотеки. Единственная проблема заключается в том, что вы не будете восстанавливать всю память, используемую незагруженной библиотекой на устройствах с уровнем API 19 и ниже (например, версии Android с использованием Dalvik VM), поскольку определения классов не собираются с мусором. Для Lollipop и позже новая виртуальная машина также будет мусором собирать определения классов, поэтому для этих устройств это будет работать лучше.

Надеюсь, это поможет.

Может быть, вы можете найти ответы здесь

Я не уверен, что это вы ищете, но он дает фактический метод деаллокации библиотеки в JVM.