【发布时间】:2014-08-05 05:00:25
【问题描述】:
动机:
我在我的 Android 应用程序中使用了一些本机库,我想在某个时间点从内存中卸载它们。当加载本机库的加载类的 ClassLoader 被垃圾收集时,库将被卸载。灵感:native unloading。
问题:
- 如果 ClassLoader 用于加载某些类(可能导致内存泄漏),则不会对其进行垃圾回收。
- 本机库只能在应用程序的一个 ClassLoader 中加载。如果旧的 ClassLoader 仍然挂在内存中的某个位置,并且新的 ClassLoader 尝试在某个时间点加载相同的本机库,则会引发异常。
问题:
- 如何以干净的方式执行本机库的卸载(卸载是我的最终目标,无论它是一种糟糕的编程技术还是类似的东西)。
- 为什么会出现内存泄漏以及如何避免?
在下面的代码中,我通过省略本地库加载代码来简化案例,只是演示了 Classloader 内存泄漏。
我正在 Android KitKat 4.4.2,API 19 上对此进行测试。设备:摩托罗拉 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。
问题是如果memoryLeak==false,那么cl2 会被垃圾回收。如果memoryLeak==true,则出现内存泄漏。这种行为与对标准 JVM 的观察不一致(我使用 [1] 中的类加载器来模拟相同的行为)。在 JVM 上,cl2 在这两种情况下都会被垃圾回收。
我还用 Eclipse MAT 分析了堆转储文件,cl2 没有被垃圾收集,因为类 EmptyClass 仍然持有对它的引用(因为类在其类加载器上持有引用)。这是有道理的。但是EmptyClass 显然不是无缘无故收集的垃圾。 GC 根路径就是这个EmptyClass。我没能说服 GC 完成EmptyClass。
memoryLeak==true 的 HeapDump 文件可以在 here 中找到,Eclipse Android 项目带有此内存泄漏的演示应用程序 here。
我还尝试了在LibClassLoader 中加载EmptyClass 的另一种变体,即Class.forName(...) 或cl2.findClass()。有/没有静态初始化,结果总是一样的。
我查了很多网上资源,据我所知没有涉及到静态缓存字段。我检查了PathClassLoader 的源代码和它的父类,我没有发现任何问题。
非常感谢您的见解和任何帮助。
免责声明:
- 我接受这不是最好的处理方式,如果有更好的选项来卸载本机库,我会非常乐意使用该选项。
- 我接受一般来说我不能依赖在某个时间窗口内调用 GC。即使调用
System.gc()也只是为JVM/Dalvik 执行GC 的提示。我只是想知道为什么会出现内存泄漏。
2015 年 11 月 11 日编辑
为了让 Erik Hellman 写的更清楚,我说的是加载 NDK 编译的 C/C++ 库,动态链接,带有 .so 后缀。
【问题讨论】:
-
只是一个建议,你有没有试过改变这一行 this.getApplicationContext();到 className.getApplicatonContext()?
标签: android memory-leaks garbage-collection classloader native-code