ClassLoader详解

大家都知道,一个Java程序编译之后,是由若干个.class文件组织而成的一个完整的Java应用程序,当程序在运行时,即会调用该程序的一个入口函数来调用系统的相关功能,而这些功能都被封装在不同的class文件当中,所以经常要从这个class文件中要调用另外一个class文件中的方法,如果另外一个文件不存在的,则会引发系统异常。而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的。

Java默认提供的三个ClassLoader

  • BootStrap ClassLoader: 称为启动类加载器,是Java类加载层次中最顶层的类加载器,负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等
  • Extension ClassLoader: 称为扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar
  • App ClassLoader: 称为系统类加载器,负责加载应用程序classpath目录下的所有jar和class文件

双亲委托模型

简化的ClassLoader.java

public abstract class ClassLoader {
    private ClassLoader parent;

    ...

    public Class<?> loadClass(String className) throws ClassNotFoundException {
        return loadClass(className, false);
    }

    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);

        if (clazz == null) {
            ClassNotFoundException suppressed = null;
            try {
                clazz = parent.loadClass(className, false);
            } catch (ClassNotFoundException e) {
                suppressed = e;
            }

            if (clazz == null) {
                try {
                    clazz = findClass(className);
                } catch (ClassNotFoundException e) {
                    e.addSuppressed(suppressed);
                    throw e;
                }
            }
        }

        return clazz;
    }

    protected Class<?> findClass(String className) throws ClassNotFoundException {
        throw new ClassNotFoundException(className);
    }

    ...
}

ClassLoader体系架构

  • 自底向上检查类是否加载
  • 自顶向下尝试加载类
    ClassLoader体系结构

加载过程

  • 当前ClassLoader首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。
  • 每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。
  • 当前classLoader的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到bootstrp ClassLoader.
  • 当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。

Java为什么要采用这样的委托机制?

  • 理解这个问题,我们引入另外一个关于Classloader的概念命名空间
  • 它是指要确定某一个类,需要类的全限定名以及加载此类的ClassLoader来共同确定。
  • 也就是说即使两个类的全限定名是相同的,但是因为不同的ClassLoader加载了此类,那么在JVM中它是不同的类。

明白了命名空间以后,我们再来看看委托模型。

  • 采用了委托模型以后加大了不同的 ClassLoader的交互能力,比如上面说的,我们JDK本生提供的类库,比如hashmap,linkedlist等等,这些类由bootstrp 类加载器加载了以后,无论你程序中有多少个类加载器,那么这些类其实都是可以共享的,这样就避免了不同的类加载器加载了同样名字的不同类以后造成混乱。
  • 再比如假设有一个开发者自己编写了一个名为Java.lang.Object的类,想借此欺骗JVM。现在他要使用自定义ClassLoader来加载自己编写的java.lang.Object类。然而幸运的是,双亲委托模型不会让他成功。因为JVM会优先在Bootstrap ClassLoader的路径下找到java.lang.Object类,并载入它。思考: 如果使用自定义的类打破了双亲模式呢, 会不会就成功了呢? (下文给出答案)

Java的类加载是否一定遵循双亲委托模型?

  • 这个答案是否定的。
  • 双亲委托模型只是JDK提供的ClassLoader类的实现方式。
  • 在实际开发中,我们可以通过自定义ClassLoader,并重写父类的loadClass方法,来打破这一机制。

自定义ClassLoader

在自定义Classloader的时候,我们需要注意几个重要的方法

loadClass 方法

protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
    Class<?> clazz = findLoadedClass(className);

    if (clazz == null) {
        ClassNotFoundException suppressed = null;
        try {
            clazz = parent.loadClass(className, false);
        } catch (ClassNotFoundException e) {
            suppressed = e;
        }

        if (clazz == null) {
            try {
                clazz = findClass(className);
            } catch (ClassNotFoundException e) {
                e.addSuppressed(suppressed);
                throw e;
            }
        }
    }

    return clazz;
}
  • 需要注意public Class<?>loadClass(String name) throws ClassNotFoundException没有被标记为final, 也就意味着我们是可以override这个方法的,也就是说双亲委托机制是可以打破的。

findClass 方法

protected Class<?> findClass(String className) throws ClassNotFoundException {
    throw new ClassNotFoundException(className);
}
  • 我们可以看出此方法默认的实现是直接抛出异常
  • 其实这个方法就是留给我们应用程序来override的。
  • 那么具体的实现就看你的实现逻辑了,你可以从磁盘读取,也可以从网络上获取class文件的字节流,获取class二进制了以后就可以交给defineClass来实现进一步的加载。
  • defineClass我们再下面再来描述。
  • ok,通过上面的分析,我们可以得出结论: 我们在写自己的ClassLoader的时候,如果想遵循双亲委托机制,则只需要overridefindClass.

defineClass 方法

protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError {     
    return defineClass(name, b, off, len, null);
}
  • 从上面的代码我们看出此方法被定义为了final,这也就意味着此方法不能被Override,其实这也是jvm留给我们的唯一的入口,通过这个唯一的入口,jvm保证了类文件必须符合Java虚拟机规范规定的类的定义。此方法最后会调用native的方法来实现真正的类的加载工作。
  • 思考问题
    • 假如我们自己写了一个java.lang.String的类,我们是否可以替换调JDK本身的类?
    • 答案是否定的。我们不能实现。
    • 为什么呢?我看很多网上解释是说双亲委托机制解决这个问题,其实不是非常的准确。因为双亲委托机制是可以打破的,你完全可以自己写一个classLoader来加载自己写的java.lang.String类,但是你会发现也不会加载成功,具体就是因为针对java.*开头的类,jvm的实现中已经保证了必须由bootstrp来加载。

总结: 如何实现自定义ClassLoader

  1. 继承java.lang.ClassLoader
  2. 重写父类的findClass方法

Android平台的ClassLoader

  • ClassLoader继承 树

ClassLoader

  • java.lang.ClassLoader是所有ClassLoader的最终父类
  • Android中默认无父构造器传入的情况下,默认父构造器为一个PathClassLoader且此PathClassLoader父构造器为BootClassLoader。
  • ClassLoader中重要的方法是loadClass(String name),其他的子类都继承了此方法且没有进行复写。它实现了双亲委托模型, 这种模型并不是一个强制性的约束模型,比如你可以继承ClassLoader复写loadCalss方法来破坏这种模型,只不过双亲委派模是一种被推荐的实现类加载器的方式,而且jdk1.2以后已经不提倡用户在覆盖loadClass方法,而应该把自己的类加载逻辑写到findClass中

BootClassLoader

  • 和java虚拟机中不同的是BootClassLoader是ClassLoader内部类,由java代码实现而不是c++实现,是Android平台上所有ClassLoader的最终parent,这个内部类是包内可见,所以我们没法使用。

URLClassLoader

  • 只能用于加载jar文件,但是由于 dalvik 不能直接识别jar,所以在 Android 中无法使用这个加载器。

BaseDexClassLoader

  • PathClassLoader和DexClassLoader都继承自BaseDexClassLoader,其中的主要逻辑都是在BaseDexClassLoader完成的。这些源码在java/dalvik/system中。
  • 先看下BaseDexClassLoader的构造方式:
    • BaseDexClassLoader构造方法
  • BaseDexClassLoader的构造函数包含四个参数,分别为:
    • dexPath 指目标类所在的APK或jar文件的路径,类装载器将从该路径中寻找指定的目标类,该类必须是APK或jar的全路径.如果要包含多个路径,路径之间必须使用特定的分割符分隔,特定的分割符可以使用System.getProperty(“path.separtor”)获得。上面”支持加载APK、DEX和JAR,也可以从SD卡进行加载”指的就是这个路径,最终做的是将dexPath路径上的文件ODEX优化到内部位置optimizedDirectory,然后,再进行加载的。
    • File optimizedDirectory 由于dex文件被包含在APK或者Jar文件中,因此在装载目标类之前需要先从APK或Jar文件中解压出dex文件,该参数就是制定解压出的dex 文件存放的路径。这也是对apk中dex根据平台进行ODEX优化的过程。其实APK是一个程序压缩包,里面包含dex文件,ODEX优化就是把包里面的执行程序提取出来,就变成ODEX文件,因为你提取出来了,系统第一次启动的时候就不用去解压程序压缩包的程序,少了一个解压的过程。这样的话系统启动就加快了。为什么说是第一次呢?是因为DEX版本的也只有第一次会解压执行程序到 /data/dalvik-cache(针对PathClassLoader)或者optimizedDirectory(针对DexClassLoader)目录,之后也是直接读取目录下的的dex文件,所以第二次启动就和正常的差不多了。当然这只是简单的理解,实际生成的ODEX还有一定的优化作用。ClassLoader只能加载内部存储路径中的dex文件,所以这个路径必须为内部路径。
    • libPath 指目标类中所使用的C/C++库存放的路径
    • classload 是指该装载器的父装载器,一般为当前执行类的装载器,例如在Android中以context.getClassLoader()作为父装载器。

DexClassLoader

  • 再看下DexClassLoader构造方法:
    • DexClassLoader构造方法
  • DexClassLoader支持加载APK、DEX和JAR,也可以从SD卡进行加载。
  • 上面说dalvik不能直接识别jar,DexClassLoader却可以加载jar文件,这难道不矛盾吗?
  • 其实在BaseDexClassLoader里对”.jar”,”.zip”,”.apk”,”.dex”后缀的文件最后都会生成一个对应的dex文件,所以最终处理的还是dex文件,而URLClassLoader并没有做类似的处理。
  • 一般我们都是用这个DexClassLoader来作为动态加载的加载器。

PathClassLoader

  • 构造方法如下
  • 很简单明了,可以看出PathClassLoader没有将optimizedDirectory置为Null,也就是没设置优化后的存放路径。其实optimizedDirectory为null时的默认路径就是/data/dalvik-cache 目录。
  • PathClassLoader是用来加载Android系统类和应用的类,并且不建议开发者使用。

BaseDexClassLoader加载class的过程

#BaseDexClassLoader
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    Class clazz = pathList.findClass(name);
    if (clazz == null) {
        throw new ClassNotFoundException(name);
    }
    return clazz;
}
#DexPathList
public Class findClass(String name) {
    for (Element element : dexElements) {
        DexFile dex = element.dexFile;
        if (dex != null) {
            Class clazz = dex.loadClassBinaryName(name, definingContext);
          if (clazz != null) {
              return clazz;
          }
        }
    }
    return null;
}
#DexFile
public Class loadClassBinaryName(String name, ClassLoader loader) {
    return defineClass(name, loader, mCookie);
}
private native static Class defineClass(String name, ClassLoader loader, int cookie);
  • 可以看出,BaseDexClassLoader中有个pathList对象,pathList中包含一个DexFile的数组dexElements
  • 由上面分析知道,dexPath传入的原始dex(.apk,.zip,.jar等)文件在optimizedDirectory文件夹中生成相应的优化后的odex文件,dexElements数组就是这些odex文件的集合,如果不分包一般这个数组只有一个Element元素,也就只有一个DexFile文件
  • 而对于类加载呢,就是遍历这个集合,通过DexFile去寻找。最终调用native方法的defineClass。

总结

对于我来说, 写文章感觉最后不写个总结很别扭, 所以在这里再写一个总结, 其实也没什么可写的, 就简单的说两句吧, 这篇主要是一个入门, 写了Java中的ClassLoader和双亲委托模型, 然后简单的介绍了一下Android中的ClassLoader, 总感觉意犹未尽, 以后有时间, 再来好好的研究一下Android中的类加载,和dexopt, dex2oat这些东东吧.

文章目录
  1. 1. Java默认提供的三个ClassLoader
  2. 2. 双亲委托模型
    1. 2.1. 简化的ClassLoader.java
    2. 2.2. ClassLoader体系架构
    3. 2.3. 加载过程
    4. 2.4. Java为什么要采用这样的委托机制?
    5. 2.5. Java的类加载是否一定遵循双亲委托模型?
  3. 3. 自定义ClassLoader
    1. 3.1. loadClass 方法
    2. 3.2. findClass 方法
    3. 3.3. defineClass 方法
    4. 3.4. 总结: 如何实现自定义ClassLoader
  4. 4. Android平台的ClassLoader
    1. 4.1. ClassLoader
    2. 4.2. BootClassLoader
    3. 4.3. URLClassLoader
    4. 4.4. BaseDexClassLoader
    5. 4.5. DexClassLoader
    6. 4.6. PathClassLoader
    7. 4.7. BaseDexClassLoader加载class的过程
  5. 5. 总结