关于ClassLoader,你需要了解的

Java中的ClassLoader

Java中包含三种系统类加载器,分别是Bootstrap ClassLoader、Extensions ClassLoader和Application ClassLoader。除此之外,Java还支持自定义的ClassLoader。

Bootstrap ClassLoader

Bootstrap ClassLoader也称为引导类加载器,它是由C/C++代码实现的类加载器,用于加载JDK的核心类库,比如java.lang、java.uti等这些系统类。它用来加载以下目录下的类库:

  • $JAVA_HOME/jre/lib目录
  • -Xbootclasspath参数指定的目录

Java虚拟机的启动就是通过Bootstrap ClassLoader创建第一个初始类来完成的。由于Bootstrap ClassLoader是用C/C++语言实现的,所以该加载器不能被Java代码访问到。需要注意的是,Bootstrap ClassLoader并不是继承自java.lang.ClassLoader。

Extensions ClassLoader

Extensions ClassLoader也称为扩展类加载器。在Java中的实现类是ExtClassLoader。它用于加载Java的扩展类,提供除了系统类之外的一些功能。ExtClassLoader用来加载以下目录下的类库:

  • $JAVA_HOME/jre/lib/ext目录
  • 系统属性java.ext.dir所指定的目录
Application ClassLoader

也称为应用程序类加载器,在java中的实现类是AppClassLoader,因此简称AppClassLoader。同时它又可以称为系统类加载器(System ClassLoader),这是因为AppClassLoader可以通过ClassLoader.getSystemClassLoader方法获取到。它用来加载以下目录下的类库:

  • 当前程序的Classpath目录
  • 系统属性java.class.path指定的目录
自定义ClassLoader

自定义ClassLoader通过继承java.lang.ClassLoader类的方式来实现自己的类加载器。ExtCLassLoader和AppClassLoader也继承自java.lang.ClassLoader。实现自定义ClassLoader需要两个步骤

  1. 定义一个自定义ClassLoader并继承抽象类ClassLoader。
  2. 重新findClass方法,并在方法中调用defineClass方法。

ClassLoader的继承关系

运行一个Java程序需要用到几种类型的ClassLoader呢

IGame the_last_of_us = new IGame();
        ClassLoader loader = the_last_of_us.getClass().getClassLoader();
        while(loader != null){
            System.out.println(loader);
            loader = loader.getParent();
        }

通过运行以上代码可以看到,输出结果是

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@135fbaa4

第一行说明加载IGame类的是AppClassLoader,第二行输出说明AppClassLoader通过getParent()方法获取到的是ExtClassLoader,这并不表示它们是父类和子类的继承关系,只能说ExtClassLoader是AppClassLoader的父加载器。至于为何没有打印出ExtClassLoader的父加载器Bootstrap ClassLoader,只是因为Bootstrap ClassLoader是由C/C++语言实现的,并不是一个Java类,无法在Java代码中获取它的引用。系统提供的类加载器有这三种类型,但并不是说系统系统的ClassLoader只有这3个。ClassLoader的继承关系如下图所示:
Java中ClassLoader的继承关系

可以看出一共有5个相关类:

  • ClassLoader是一个抽象类,其中定义了ClassLoader的主要功能
  • SecureClassLoader继承了抽象类ClassLoader,但SecureClassLoader并不是ClassLoader的实现类,而是扩展了ClassLoader类,加入了权限方面的功能,加强了ClassLoader的安全性。
    URLClassLoader继承自SecureClassLoader,可以通过URL路径从jar文件和文件夹中加载类和资源
  • ExtClassLoader和AppClassLoader都是继承自URLClassLoader,他们都是Launcher的内部类,Launcher是Java虚拟机的入口应用,ExtClassLoader和AppClassLoader都是在Launcher中进行初始化的。

双亲委派模式

类加载器查找Class所采用的是双亲委派模式,所谓双亲委派模式,就是首先判断该Class是否已经加载,如果没有并不是该加载器直接去查找,而是委托给父加载器进行查找,这样依次递归直到委托到最顶层的Bootstrap ClassLoader,如果Bootstrap ClassLoader找到了该Class,就会直接加载并返回。如果没找到,则继续依次向下查找,如果还没找到则最后会交给最初判断的类加载器去查找。具体过程如下图所示:
双亲委托模式

以一个具体的例子来说,假设我们要加载一个位于D盘的Class文件,这时系统提供的类加载器不能满足条件,这时就需要我们自定义类加载器继承自java.lang.ClassLoader。并重写它的findClass方法。按照双亲委派流程,加载过程如下:

  1. 自定义的类加载器首先在缓存中查找Class文件是否已经加载,如果已经加载就返回该Class。否则就委托给父加载器,也就是AppClassLoader。
  2. 按照上图中虚线的方向递归步骤一,即AppClassLoader在缓存中查找该Class文件是否已经加载,如果已经加载就返回该Class。否则就委托给它的父加载器ExtClassLoader。
  3. 一直委托到Bootstrap ClassLoader,如果BootStrap ClassLoader查找缓存也没有加载该Class文件,则在$JAVA_HOME/jre/lib目录或者-Xbootclasspath参数指定的目录中进行查找,如果找到就加载该Class并返回。如果没有找到则交给它的子加载器ExtClassLoader。
  4. ExtClassLoader会在$JAVA_HOME/jre/lib/ext目录或系统属性java.ext.dir所指定的目录中进行查找,如果找到该Class就加载并返回。否则就交给AppClassLoader。
  5. AppClassLoader会在当前程序的Classpath目录或系统属性java.class.path指定的目录中进行查找,如果找到该Class就加载并返回,找不到则交给我们自定义的类加载器,如果还找不到就会抛出异常。

总的来说就是Class文件加载到ClassLoader子系统后,先沿着上图中虚线方向自下而上进行委托,判断该Class是否已加载,如果没有加载,再沿着实线方向自上而下进行查找和加载。结合上一节讲的ClassLoader的继承关系,可以得出ClassLoader子系统中的父子关系并不是使用继承实现,而是使用组合来实现代码复用的。
类加载的过程在JDK8的源码中也能看出双亲委派模式的逻辑实现。以下是抽象类ClassLoader的loadClass方法的源码。

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

首先调用findLoadedClass从已加载的类中检查目标类是否已加载,如果未加载即c为空,判断parent(父加载器)是否存在,如果存在就调用父加载器的loadClass方法,否则就调用findBootstrapClassOrNull方法,这个方法内部会调用Native方法findBootstrapClass,findBootstrapClass方法中最终会用Bootstrap ClassLoader来检查目标类是否已经加载,如果没有加载就说明向上委托流程中没有发现目标类已加载。然后调用findClass方法继续向下进行查找流程。
采取双亲委派模式主要有两个好处:

  • 避免重复加载,如果该Class已经加载过一次,就不需要再次加载,而是从缓存中读取已经加载的Class信息。
  • 更加安全,如果不使用双亲委派模式,就可以自定义一个String类来替代系统的String类,这显然是会造成安全隐患的,采用双亲委派模式会是的系统的String类在Java虚拟机启动时就被加载,也就无法用自定义的String类来替代系统的String类。除非我们修改类加载器的默认搜索算法。还有一点,只有两个类名一致且被同一个类加载器加载的类,JVM才会认为它们是同一个类,想要骗过JVM显然没那么容易。

Android中的ClassLoader

可能有些同学认为,Android中的ClassLoader和Java中的ClassLoader是一样的,这显然是不对的。下文中会介绍两者之间有何不同。

ClassLoader的类型

Java中的ClassLoader可以加载class文件和jar文件,本质上都是加载class文件。这一点在Android中并不适用,因为无论是DVM还是ART,他们加载的不再是class文件,而是dex文件,这就需要重新设计ClassLoader的相关类。Android中的ClassLoader有3种系统默认的类型,分别是BootClassLoader、PathClassLoader和DexClassLoader,除此之外,Android也支持自定义的ClassLoader。

BootClassLoader

Android系统启动时会使用BootClassLoader来预加载常用类,与JDK中的Bootstrap ClassLoader不同,它并不是由C/C++代码实现的,而是用Java代码实现的。BootClassLoader的代码如下

class BootClassLoader extends ClassLoader {

    private static BootClassLoader instance;

    @FindBugsSuppressWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED")
    public static synchronized BootClassLoader getInstance() {
        if (instance == null) {
            instance = new BootClassLoader();
        }

        return instance;
    }
...

BootClassLoader是ClassLoader的内部类,这里的ClassLoader并不是JDK里的ClassLoader,这一点需要注意。BootClassLoader是一个单例类,需要注意的是BootClassLoader的访问修饰符是默认的,即只有同一个包中的类才能访问,因此我们在应用程序中是无法直接调用的。关于BootClassLoader的创建,BootClassLoader是在Zygote进程的ZygoteInit的入口方法中创建的,用于加载preloaded-classes文件中存有的预加载类。

DexClassLoader

DexClassLoader可以加载dex文件以及包含dex的压缩文件,如apk文件和jar文件,不管是哪种文件,最终要加载的都是dex文件。查看DexClassLoader的源码如下

public class DexClassLoader extends BaseDexClassLoader {
    /**
     * Creates a {@code DexClassLoader} that finds interpreted and native
     * code.  Interpreted classes are found in a set of DEX files contained
     * in Jar or APK files.
     *
     * <p>The path lists are separated using the character specified by the
     * {@code path.separator} system property, which defaults to {@code :}.
     *
     * @param dexPath the list of jar/apk files containing classes and
     *     resources, delimited by {@code File.pathSeparator}, which
     *     defaults to {@code ":"} on Android
     * @param optimizedDirectory this parameter is deprecated and has no effect since API level 26.
     * @param librarySearchPath the list of directories containing native
     *     libraries, delimited by {@code File.pathSeparator}; may be
     *     {@code null}
     * @param parent the parent class loader
     */
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

DexClassLoader类的定义中只包含一个构造方法,有4个参数:

  • dexPath:dex相关文件的路径集合,多个路径用文件分隔符隔开,默认的分隔符是“:”。
  • optimizedDirectory:解压的dex文件存储路径,这个路径必须是一个内部存储路径。一般情况下,使用当前应用程序的私有路径:/data/data/<Package Name>/...
  • librarySearchPath:包含C/C++库的路径集合,多个路径用文件分隔符隔开,可以为null。
  • parent:父加载器

DexClassLoader继承自BaseDexClassLoader,方法都在BaseDexClassLoader中实现。

PathClassLoader

Android系统使用PathClassLoader来加载系统类和应用程序的类,查看其代码如下

public class PathClassLoader extends BaseDexClassLoader {
    /**
     * Creates a {@code PathClassLoader} that operates on a given list of files
     * and directories. This method is equivalent to calling
     * {@link #PathClassLoader(String, String, ClassLoader)} with a
     * {@code null} value for the second argument (see description there).
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param parent the parent class loader
     */
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    /**
     * Creates a {@code PathClassLoader} that operates on two given
     * lists of files and directories. The entries of the first list
     * should be one of the following:
     *
     * <ul>
     * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
     * well as arbitrary resources.
     * <li>Raw ".dex" files (not inside a zip file).
     * </ul>
     *
     * The entries of the second list should be directories containing
     * native library files.
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param librarySearchPath the list of directories containing native
     * libraries, delimited by {@code File.pathSeparator}; may be
     * {@code null}
     * @param parent the parent class loader
     */
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

PathClassLoader也继承自BaseDexClassLoader,方法也都在BaseDexClassLoader中实现。在PathDexClassLoader的构造方法中没有optimizedDirectory参数,这是因为PathClassLoader已经默认设置了optimizedDirectory参数的值为/data/dalvik-cache,这也意味着PathClassLoader无法定义解压的dex文件存储路径,因此PathClassLoader通常用来加载已经apk的dex文件,已安装的apk的dex文件会存储在/data/dalvik-cache目录中。PathClassLoader的创建,是在Zygote进程创建SystemServer进程后,在SystemServer进程中采用工厂模式创建的。

ClassLoader的继承关系

Android中ClassLoader的继承关系

从图中可以看出一共有8个ClassLoader相关类,其中有一些和Java中的ClassLoader相关类十分相似。

  • ClassLoader 是一个抽象类,其中定义了ClassLoader的主要功能。BootClassLoader是它的内部类。
  • SecureClassLoader类和JDK8中的SecureClassLoader类的代码一样,它继承了抽象类ClassLoader。SecureClassLoader并不是ClassLoader的实现类,而是扩展了ClassLoader类,加入了权限方面的功能,加强了ClassLoader的安全性。
  • URLClassLoader类和JDK8中的URLClassLoader类的代码一样,他继承自SecureClassLoader,用来通过URL路径从jar文件和文件夹中加载指定的资源。
  • InMemoryDexClassLoader 是Android8.0新增的CLassLoader,继承自BaseDexClassLoader,用来加载内存中的dex文件
  • BaseDexClassLoader继承自ClassLoader,是抽象类ClassLoader的具体实现类,PathCLassLoader、DexClassLoader和InMemoryDexClassLoader都是继承自BaseDexClassLoader。

ClassLoader的加载过程

Android中的ClassLoader同样遵循双亲委派模式,ClassLoader的加载方法为loadClass方法,这个方法定义在抽象类ClassLoader中,代码如下:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

可以看出,loadClass方法的代码逻辑和JDK中ClassLoader类的loadClass方法非常相似,此处不再赘述,重点在findClass方法。

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

抽象类ClassLoader的findClass方法中直接抛出了异常,说明findClass方法需要子类来实现,BaseDexClassLoader的代码如下:

public class BaseDexClassLoader extends ClassLoader {
  ...
  private final DexPathList pathList;
  ...
  public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
        // TODO We should support giving this a library search path maybe.
        super(parent);
        this.pathList = new DexPathList(this, dexFiles);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException(
                    "Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }
  ...
}

在BaseDexClassLoader的构造方法中,创建了DexPathList。在findClass方法中调用了DexPathList的findClass方法。代码如下

public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }

        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

DexPathList的findClass方法中会遍历Element类型的数组dexElements,然后调用Element的findClass方法。Element是DexPathList的内部类:

static class Element {
        /**
         * A file denoting a zip file (in case of a resource jar or a dex jar), or a directory
         * (only when dexFile is null).
         */
        private final File path;

        private final DexFile dexFile;

        private ClassPathURLStreamHandler urlHandler;
        private boolean initialized;

        /**
         * Element encapsulates a dex file. This may be a plain dex file (in which case dexZipPath
         * should be null), or a jar (in which case dexZipPath should denote the zip file).
         */
        public Element(DexFile dexFile, File dexZipPath) {
            this.dexFile = dexFile;
            this.path = dexZipPath;
        }

        public Element(DexFile dexFile) {
            this.dexFile = dexFile;
            this.path = null;
        }

        public Element(File path) {
          this.path = path;
          this.dexFile = null;
        }
        ...
        public Class<?> findClass(String name, ClassLoader definingContext,
                List<Throwable> suppressed) {
            return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                    : null;
        }
}

从Element的构造方法来看,其内部封装了DexFile,每一个Element对象都包含一个DexFile对象,它用来加载dex文件。Element的findClass方法内部判断,如果DexFile不为null就调用DexFile的loadClassBinaryName方法。

public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
        return defineClass(name, loader, mCookie, this, suppressed);
    }
private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                     DexFile dexFile, List<Throwable> suppressed) {
        Class result = null;
        try {
            result = defineClassNative(name, loader, cookie, dexFile);
        } catch (NoClassDefFoundError e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        } catch (ClassNotFoundException e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        }
        return result;
    }

DexFile的loadClassBinaryName方法直接调用了defineClass方法,而在defineClass方法内部调用了defineClassNative方法来加载dex文件,这个方法是个Native方法,有兴趣的同学可以自行查看其源码。Android中ClassLoader的加载就是遵循着双亲委派模式,如果委托过程中没有检查到此前加载过目标类,就调用ClassLoader的findClass方法,在Java层最终会调用DexFIle的defineClassNative方法来执行查找流程。

本文参考
《Android进阶解密》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349