JVM系列(二):Java类加载器和双亲委派机制详解

上一篇文章讲了类加载过程,而类加载过程中类加载器使用是必不缺少的一部分,理解类加载过程和类加载器有助于我们更好的理解类加载子系统。这一次我们详细讲讲类加载器、自定义类加载器、双亲委派机制和打破双亲委派机制。

一、类加载器

类加载器分类

JDK默认提供以下几种ClassLoader:

引导类加载器(Bootstrap ClassLoader)
  • 引导类加载器是用C++语言写的,它是在Java虚拟机启动后初始化的

  • 负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等

扩展类加载器(Extension ClassLoader)
  • 扩展类加载器是用Java写的,初始化时间在引导类加载器之后,具体初始化过程可以参考上一篇文章
  • 负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
  • 扩展类加载器的父类加载器为引导类加载器
应用类加载器(Application ClassLoader)
  • 扩展类加载器也是用Java写的,初始化时间在扩展类加载器之后
  • 负责加载ClassPath路径下的类包,主要就是加载自己写的那些未指定类加载器的类
  • 应用类加载器的父类加载器为扩展类加载器
自定义类加载器(Customer ClassLoader)
  • 扩展类加载器也是用Java写的,初始化时间在应用类加载器之后

  • 负责加载用户自定义路径下的类包

  • 自定义类加载器的父类加载器为应用类加载器

请添加图片描述

类加载器的初始化

类加载器的创建主要分为**引导类加载器(Bootstrap ClassLoader)**初始化、**扩展类加载器(Extension ClassLoader)**初始化、**应用类加载器(Application ClassLoader)**三部分。具体过程如下图所示:

请添加图片描述

可以看出类加载器初始化可以分为以下几步:

  • 创建JVM虚拟机的的过程中会先创建引导类加载器,这部分代码是C++实现的,以后有机会再展开讲
  • 随后会由引导类加载器加载Launcher类,并调起Launcher.getLauncher(),而其余类加载器的创建在Launcher()空构造方法之中,具体过程如下方代码所示
  • 扩展类加载器会先行构建,并设置父加载器为null,而null指代的是引导类加载器
  • 应用类加载器会在扩展类加载器创建完毕后创建,并设置父加载器为扩展类加载器

为扩展类加载器和应用类加载器分别设置父加载器的目的是方便双亲委派时调用,这个在下一篇文章中会讲到

public Launcher() {
    // 创建扩展类加载器
    ClassLoader extcl;
    try {
        //在构造的过程中将其父加载器设置为null(即引导类加载器)
        extcl = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException e) {
        throw new InternalError(
            "Could not create extension class loader", e);
    }

    // 创建用于启动程序的应用类加载器
    try {
        /*
         * 在构造的过程中将其父加载器设置为ExtClassLoader
         * Launcher的loader属性值是AppClassLoader
         * 我们一般都是用这个类加载器来加载我们自己写的应用程序
         */
        loader = Launcher.AppClassLoader.getAppClassLoader(extcl);
    } catch (IOException e) {
        throw new InternalError(
            "Could not create application class loader", e);
    }

    // 将当前线程上下文的类加载器设置为loader,即AppClassLoader
    Thread.currentThread().setContextClassLoader(loader);

    // 最后加载安全管理器
    String s = System.getProperty("java.security.manager");
    
    // 省略部分无关的代码
}

二、类加载机制

JVM的类加载机制主要有如下3种:

全盘负责

所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。

双亲委派

所谓的双亲委派,则是先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。

缓存机制

缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。

三、双亲委派机制

双亲委派机制主要包括两个阶段委托派发。其中委托指,加载类前会先委托父类加载器加载。其中派发指,父类加载器加载失败时向下派发,由子类加载器加载。

双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载

设计双亲委派的目的

  • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改
  • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性

双亲委派流程及源码分析

双亲委派主要存在于类加载器加载类的过程中,代码部分在ClassLoader的loadClass方法,代码流程如下图所示:

请添加图片描述

单纯从上图看起来会有些抽象,接下来结合代码具体看看ClassLoader的loadClass方法执行逻辑:

/**
 * 该方法属于ClassLoader的loadClass方法
 *
 * 该方法用于加载具有指定二进制名称(binary name)的类。 此方法的默认实现按以下顺序搜索类:
 * 1.调用findLoadedClass(String)来检查类是否已经加载。
 * 2.在父类加载器上调用loadClass方法。 如果parent为null ,则使用引导类加载器。
 * 3.调用findClass(String)方法来查找类。
 *
 * 二进制名称(binary name)
 * 作为String参数提供给ClassLoader中的方法的任何类名称必须是Java™ 语言规范定义的二进制名称。
 * 如:"java.lang.String"
 * 
 * @param  name 类的二进制名称(binary name)
 * @param  resolve 是否需要解决该类,一般为false
 * @return  二进制名称(binary name)对应的Class对象
 * @throws  ClassNotFoundException 如果类未被找到,会抛出ClassNotFoundException
 */
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException {
    // 基于类的binary name,对相同名称的类加锁,防止多个线程重复加载
    synchronized (getClassLoadingLock(name)) {
        // 首先,先检查类是否已被加载,避免重复加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // parent(父类加载器)为null则表明parent为Bootstrap ClassLoader(引导类加载器)
                if (parent != null) {
                    // 调用parent的loadClass方法加载,即向上委托
                    c = parent.loadClass(name, false);
                } else {
                    // 通过引导类加载器加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 对于不是引导类加载器的类加载器,加载失败会抛出ClassNotFoundException
            }
            // 以上,向上委托方面的代码完毕,下方是向下委托方面的代码
            if (c == null) {
                long t1 = System.nanoTime();
                // 如果交由父加载器仍无法加载该类,递归调用外层的findClass方法调用
                // 都会调用URLClassLoader的findClass方法在加载器的类路径里查找并加载该类
                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;
    }
} 

四、自定义类加载器

通常意义上来说,自定义类加载器是主要是修改获取类的方式,即自定义findClass方法

自定义类加载器主要是继承 java.lang.ClassLoader 类。该类有两个核心方法,一个是loadClass(String, boolean),实现了双亲委派机制;还有一个方法是findClass,默认实现是空方法,所以我们自定义类加载器必须重写findClass方法

public class MyClassLoader extends ClassLoader {
    private String classPath;

    public MyClassLoader(String classPath) {
        // 构造方法传入需要加载class文件夹的地址
        this.classPath = classPath;
    }

    private byte[] loadByte(String name) throws Exception {
        // 替换为实际地址
        name = name.replaceAll("\\.", "/");
        // 加载class文件
        FileInputStream fis = new FileInputStream(classPath + "/" + name
                + ".class");
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            // 加载class文件
            byte[] data = loadByte(name);
            //defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }
}

/**
 * 测试类,打印类加载器名称,输出:  test.classloader.MyClassLoader
 */
public class MyClassLoaderTest {
    public static void main(String[] args) throws Exception {
        // 初始化自定义类加载器,设置文件路径,自定义类加载器的父加载器为应用类加载器(AppClassLoader)
        MyClassLoader myClassLoader = new MyClassLoader("D:\\JavaPro\\Test");
        // 找一个别的项目编译的User.class文件,放入D:\JavaPro\Test\test\bean文件夹
        Class<?> aClass = myClassLoader.loadClass("test.bean.User");
        // 打印使用的类加载器名称,运行结果输出:test.classloader.MyClassLoader
        System.out.println(aClass.getClassLoader().getClass().getName());
    }
}

为什么自定义类加载器的父加载器为应用类加载器?

自定义类加载器MyClassLoader初始化时会自动调起super(),即父类ClassLoader的空构造方法,该方法中会设置其父构造器parent为应用类加载器AppClassLoader

/**
 * MyClassLoader初始化时会自动调起super(),即ClassLoader()
 */
protected ClassLoader() {
    // 通过下方ClassLoader构造方法得知,通过getSystemClassLoader()父构造方法
    this(checkCreateClassLoader(), getSystemClassLoader());
}

private ClassLoader(Void unused, ClassLoader parent) {
    this.parent = parent;
    // 忽略部分本次讨论无关的代码
}

/**
 * 返回用于委托(双亲委派中的委托)的系统类加载器。 
 * 这是新ClassLoader实例的默认委托的父加载器parent,通常是启动应用程序的类加载器AppClassLoader。
 */
public static ClassLoader getSystemClassLoader() {
   	// scl为ClassLoader的成员变量,在initSystemClassLoader()方法中完成初始化
    initSystemClassLoader();
    if (scl == null) {
        return null;
    }
    // 后续是一些安全校验方面的逻辑,本次讨论忽略这部分
    return scl;
}

private static synchronized void initSystemClassLoader() {
    if (!sclSet) {
        // 排除些异常状况,如sclSet == true & scl == null
        if (scl != null)
            throw new IllegalStateException("recursive invocation");
        // 获取Launcher()对象,默认会调起Launcher()空构造方法
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
        if (l != null) {
            // 通过Launcher()对象来获取类加载器
            scl = l.getClassLoader();
            // 省略部分授权方面的代码
        }
        sclSet = true;
    }
}

//-------------------------以下部分为Launcher源码------------------------
/**
 * 直接返回Launcher的成员变量loader,而loader会在Launcher初始化时加载
 */
public ClassLoader getClassLoader() {
    return loader;
}

public Launcher() {
    // 忽略部分与本次讨论无关的代码
    
    try {
        // 可以看到Launcher()初始化的的时候会默认将loader设值为AppClassLoader
        loader = Launcher.AppClassLoader.getAppClassLoader(extcl);
    } catch (IOException e) {
        throw new InternalError(
            "Could not create application class loader", e);
    }

    // 忽略部分与本次讨论无关的代码
}

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值