首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >由浅入深谈 Java 的类加载机制

由浅入深谈 Java 的类加载机制

作者头像
神秘的寇先森
发布2018-05-30 16:33:49
4950
发布2018-05-30 16:33:49
举报
文章被收录于专栏:Java进阶之路Java进阶之路
本文涉及知识点:双亲委托机制、BootstrapClassLoader、ExtClassLoader、AppClassLoader等。
  • 什么是 Java 类加载机制? Java 虚拟机使用 Java 类的流程为:首先将 .java文件编译成 .class文件,然后类加载器会读取这个 .class 文件,并转换成java.lang.Class的对象。有了该 Class 实例后,Java 虚拟机可以利用 newInstance 之类的方法创建其真正对象了。 ClassLoader是 Java 提供的类加载器,绝大多数的类加载器都继承自 ClassLoader,它们被用来加载不同来源的 Class 文件。
  • Class 文件有哪些来源呢? ClassLoader 可以加载多种来源的 Class,那么具体有哪些来源呢?
  1. 是开发者编写的类,这些类位于项目目录下;
  2. 然后,有 Java 内部自带的核心类如 java.lang、java.math、java.io 等 package 内部的类,位于 $JAVA_HOME/jre/lib/ 目录下,如 java.lang.String 类就是定义在 $JAVA_HOME/jre/lib/rt.jar 文件里;
  3. 另外,还有 Java 核心扩展类,位于 $JAVA_HOME/jre/lib/ext 目录下。开发者也可以把自己编写的类打包成 jar 文件放入该目录下;
  4. 最后还有一种,是动态加载远程的 .class 文件。 既然有这么多种类的来源,那么在 Java 里,是由哪一个 ClassLoader 来统一加载呢?还是由多个 ClassLoader 来协作加载呢?
  • 哪些 ClassLoader 负责加载上面几类 Class? 针对上面四种来源的类,分别有不同的加载器负责加载。
  1. 首先,级别最高的 Java 核心类,即$JAVA_HOME/jre/lib 里的核心 jar 文件。这些类是 Java 运行的基础类,由一个名为BootstrapClassLoader加载器负责加载,它也被称作** 根加载器/引导加载器。注意,BootstrapClassLoader 比较特殊,它不继承 ClassLoader**,而是由 JVM 内部实现;
  2. 然后,需要加载 Java 核心扩展类,即 $JAVA_HOME/jre/lib/ext 目录下的 jar 文件。这些文件由ExtensionClassLoader 负责加载,它也被称作 扩展类加载器。当然,用户如果把自己开发的 jar 文件放在这个目录,也会被ExtensionClassLoader 加载;
  3. 接下来是开发者在项目中编写的类,这些文件将由 AppClassLoader类加载器进行加载,它也被称作 系统类加载器 System ClassLoader
  4. 最后,如果想远程加载如(本地文件/网络下载)的方式,则必须要自己自定义一个 ClassLoader,复写其中的 findClass() 方法才能得以实现。 因此能看出,Java 里提供了至少四类 ClassLoader 来分别加载不同来源的 Class。 那么,这几种 ClassLoader 是如何协作来加载一个类呢?

双亲委托加载方式

了解双亲委派方式之前先想一个问题:String 类是 Java 自带的最常用的一个类,现在的问题是,JVM 将以何种方式把 String class 加载进来呢?

  1. 首先,String 类属于 Java 核心类,位于 $JAVA_HOME/jre/lib 目录下。有人马上反应过来,上文中提过了,该目录下的类会由BootstrapClassLoader进行加载。没错,它确实是由BootstrapClassLoader进行加载。但这种回答的前提是你已经知道了 String 在 $JAVA_HOME/jre/lib 目录下。
  • 如果你并不知道 String 类究竟位于哪呢?或者我希望你去加载一个 unknown 的类呢? 有的朋友这时会说,那很简单,只要去遍历一遍所有的类,看看这个 unknown 的类位于哪里,然后再用对应的加载器去加载。 是的,思路很正确。那应该如何去遍历呢? 比如,可以先遍历用户自己写的类,如果找到了就用 AppClassLoader去加载;否则去遍历 Java 核心类目录,找到了就用BootstrapClassLoader去加载,否则就去遍历 Java 扩展类库,依次类推。 这种思路方向是正确的,不过存在一个漏洞。
  • 假如开发者自己伪造了一个 java.lang.String 类,即在项目中创建一个包java.lang,包内创建一个名为 String 的类,这完全可以做到。那如果利用上面的遍历方法,是不是这个项目中用到的 String 不是都变成了这个伪造的 java.lang.String 类吗?如何解决这个问题呢? 解决方法很简单,当查找一个类时,优先遍历最高级别的 Java 核心类,然后再去遍历 Java 核心扩展类,最后再遍历用户自定义类,而且这个遍历过程是一旦找到就立即停止遍历。
这种加载方式就叫做双亲委托加载方式。

把 BootstrapClassLoader 想象为核心高层领导人, ExtClassLoader 想象为中层干部, AppClassLoader 想象为普通公务员。每次需要加载一个类,先获取一个系统加载器 AppClassLoader 的实例(ClassLoader.getSystemClassLoader()),然后向上级层层请求,由最上级优先去加载,如果上级觉得这些类不属于核心类,就可以下放到各子级负责人去自行加载。

双亲委派加载方式

从以上描述中,我们可以总结出如下四点: 1、类的加载过程采用委托模式实现 2、每个 ClassLoader 都有一个父加载器。 3、类加载器在加载类之前会先递归的去尝试使用父加载器加载。 4、虚拟机有一个内建的启动类加载器(BootstrapClassLoader),该加载器没有父加载器,但是可以作为其他加载器的父加载器。

类加载器关系图

注意:这里父类加载器并不是通过继承关系来实现的,而是采用组合实现的。

下面通过几个例子来验证上面的加载方式。 在项目中创建一个名为 MusicPlayer 的类文件,内容如下:

package cn.baidu.demo;
public class MusicPlayer {

    public void print(){
        System.out.println("Hi,I am MusicPlayer");
    }
    
    private static void loadClass() throws ClassNotFoundException {
        Class<?> clazz = Class.forName("cn.baidu.demo.MusicPlayer");//创建本类的Class对象
        ClassLoader classLoader = clazz.getClassLoader(); //获得本类的类加载器
        System.out.printf("ClassLoader is "+classLoader.getClass().getSimpleName());
    }
    
    public static void main(String[] args) throws ClassNotFoundException {
        loadClass();
    }
    
}

打印结果为:

ClassLoader is AppClassLoader

可以验证,MusicPlayer 是由 AppClassLoader 进行的加载。 下面验证:AppClassLoader 的双亲真的是 ExtClassLoader 和 BootstrapClassLoader 吗? AppClassLoader 提供了一个 getParent() 的方法,来打印看看都是什么。

package cn.baidu.demo;
public class MusicPlayer {

    public void print(){
        System.out.println("Hi,I am MusicPlayer");
    }
    
    private static void printParent() throws ClassNotFoundException {
        Class<?> clazz = Class.forName("cn.baidu.demo.MusicPlayer");  //创建本类的Class对象
        ClassLoader classLoader = clazz.getClassLoader(); //获得本类的类加载器
        System.out.printf("currentClassLoader is "+ classLoader.getClass().getSimpleName());
        System.out.println();
        while (classLoader.getParent() != null) {
            classLoader = classLoader.getParent();  //获得父类类加载器
            System.out.printf("Parent is "+ classLoader.getClass().getSimpleName());
        }
}

public static void main(String[] args) throws ClassNotFoundException {
        printParent();
    }
}

打印结果为:

currentClassLoader is AppClassLoader
Parent is ExtClassLoader

能看到 ExtClassLoader 确实是 AppClassLoader 的双亲,不过却没有看到 BootstrapClassLoader。因为 BootstrapClassLoader是由 JVM 内部实现的,所以 ExtClassLoader.getParent() = null。 -问题:如果把 MusicPlayer 类挪到 $JAVA_HOME/jre/lib/ext 目录下会发生什么? ExtClassLoader 会加载$JAVA_HOME/jre/lib/ext 目录下所有的 jar 文件。那来尝试下直接把 MusicPlayer 这个类放到 $JAVA_HOME/jre/lib/ext 目录下吧。 利用下面命令可以把 MusicPlayer.java 编译打包成 jar 文件,并放置到对应目录。

javac classloader/MusicPlayer.java
jar cvf MusicPlayer.jar classloader/MusicPlayer.class
mv MusicPlayer.jar $JAVA_HOME/jre/lib/ext/

这时 MusicPlayer.jar 已经被放置与 $JAVA_HOME/jre/lib/ext 目录下,同时把之前的 MusicPlayer 删除,而且这一次刻意使用 AppClassLoader 来加载:

private static void loadClass() throws ClassNotFoundException {
    ClassLoader appClassLoader = ClassLoader.getSystemClassLoader(); // AppClassLoader
    Class<?> clazz = appClassLoader.loadClass("cn.baidu.demo.MusicPlayer");
    ClassLoader classLoader = clazz.getClassLoader();
    System.out.printf("ClassLoader is %s", classLoader.getClass().getSimpleName());
}

打印结果为:

ClassLoader is ExtClassLoader

说明即使直接用 AppClassLoader 去加载,它仍然会被 ExtClassLoader 加载到。

源码分析理解双亲委托加载机制

打开 ClassLoader 里的 loadClass() 方法,看到分析的源码。这个方法里做了下面几件事:

  1. 检查目标class是否曾经加载过,如果加载过则直接返回;
  2. 如果没加载过,把加载请求传递给 parent 加载器去加载;
  3. 如果 parent 加载器加载成功,则直接返回;
  4. 如果 parent 未加载到,则自身调用 findClass() 方法进行寻找,并把寻找结果返回。 代码如下:
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 1. 检查是否曾加载过
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 优先让 parent 加载器去加载
                    c = parent.loadClass(name, false);
                } else {
                    // 如无 parent,表示当前是 BootstrapClassLoader,调用 native 方法去 JVM 加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // 如果 parent 均没有加载到目标class,调用自身的 findClass() 方法去搜索
                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;
    }
}

// BootstrapClassLoader 会调用 native 方法去 JVM 加载
private native Class<?> findBootstrapClass(String name);

从源码可以看出,ExtClassLoader 和 AppClassLoader都继承自 ClassLoader 类,ClassLoader 类中通过 loadClass 方法来实现双亲委派机制。整个类的加载过程可分为如下三步: 1、查找对应的类是否已经加载。 2、若未加载,则判断当前类加载器的父加载器是否为空,不为空则委托给父类去加载,否则调用启动类加载器加载(findBootstrapClassOrNull 再往下会调用一个 native 方法)。 3、若第二步加载失败,则调用当前类加载器加载。

双亲委派机制能很好地解决类加载的统一性问题。对一个 Class 对象来说,如果类加载器不同,即便是同一个字节码文件,生成的 Class 对象也是不等的。也就是说,类加载器相当于 Class 对象的一个命名空间。双亲委派机制则保证了基类都由相同的类加载器加载,这样就避免了同一个字节码文件被多次加载生成不同的 Class 对象的问题。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017.12.27 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 双亲委托加载方式
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档