【问题标题】:Android ClassLoader memory leakAndroid ClassLoader 内存泄漏
【发布时间】:2014-08-05 05:00:25
【问题描述】:

动机:

我在我的 Android 应用程序中使用了一些本机库,我想在某个时间点从内存中卸载它们。当加载本机库的加载类的 ClassLoader 被垃圾收集时,库将被卸载。灵感:native unloading

问题:

  • 如果 ClassLoader 用于加载某些类(可能导致内存泄漏),则不会对其进行垃圾回收。
  • 本机库只能在应用程序的一个 ClassLoader 中加载。如果旧的 ClassLoader 仍然挂在内存中的某个位置,并且新的 ClassLoader 尝试在某个时间点加载相同的本机库,则会引发异常。

问题:

  1. 如何以干净的方式执行本机库的卸载(卸载是我的最终目标,无论它是一种糟糕的编程技术还是类似的东西)。
  2. 为什么会出现内存泄漏以及如何避免?

在下面的代码中,我通过省略本地库加载代码来简化案例,只是演示了 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();
    }
}

我有EmptyClassLibClassLoader 一起加载。

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


【解决方案1】:

也许你能找到答案here

我不确定您是否正在寻找它,但它提供了 JVM 中实际的库释放方法。

【讨论】:

    【解决方案2】:

    首先,让我们整理一下这里的术语。

    它是您想要加载的带有 JNI 绑定的本机库吗?也就是使用 Android NDK 在 C/C++ 中实现的后缀为 .so 的文件?这通常是我们在谈论原生库时所指的。如果是这种情况,那么解决此问题的唯一方法是在单独的进程中运行库。最简单的方法是创建一个 Android 服务,在其中为清单中的条目添加 android:process=":myNativeLibProcess"。然后,该服务将照常调用System.loadLibrary(),并且您使用Context.bindService() 从您的主进程绑定到该服务。

    如果它是 JAR 文件中的一组 Java 类,那么我们正在寻找其他东西。对于 Android,您需要创建将库代码编译成 DEX 文件,该文件被放入 JAR 文件并使用 DexClassLoader 加载,类似于您在代码中所做的。 当您想卸载库时,您需要释放对您创建的实例的所有引用以及用于加载库的类加载器。这将允许您稍后加载库的新版本。唯一的问题是,您不会在 API 级别 19 或更低级别的设备(即使用 Dalvik VM 的 Android 版本)上回收卸载库使用的所有内存,因为类定义不会被垃圾收集。对于 Lollipop 及更高版本,新 VM 还将垃圾收集类定义,因此对于这些设备,这将更好地工作。

    希望对您有所帮助。

    【讨论】:

    • 你对 OP 观察到的行为有什么解释吗?
    • 很难说清楚,但正如我在回答中所写的那样,即使类加载器被释放,类定义也不会被垃圾收集(棒棒糖之前)。这可能是原因,因为 OP 在 KitKat 上对此进行了测试。在 Lollipop 上进行测试应该确认是否是导致“泄漏”的类定义。
    • 感谢您的回答。我编辑了我的原始问题以明确我的意思是 NDK 编译的 C/C++ 库。使用完全不同的进程是一个很好的解决方案,通过杀死进程内存会被释放,但目前它对我们来说不可用,因为一些移动设备管理系统不支持具有多个进程的应用程序。不过还是谢谢!
    • 支持 Android 的 MDM 甚至不应该知道多进程应用程序。您需要做的就是将android:process=":subProcess" 添加到Service,就完成了。
    • @ErikHellman 最后怎么杀掉android子进程,能不能启动多个子进程?
    猜你喜欢
    • 2012-12-06
    • 2012-02-09
    • 2011-12-31
    • 1970-01-01
    • 1970-01-01
    • 2012-11-14
    • 2015-07-20
    • 2013-08-23
    相关资源
    最近更新 更多