java 类加载之双亲委派


作为一名java程序员,肯定都会知道类的加载过程分为:加载、链接、初始化,不知道也没有关系,可以关注我公众号去找「java类加载过程」这篇文章进行查看,本篇主要说一下加载的第一步「装载」的详细过程-双亲委派机制。

image.png

  1. 什么是双清委派机制

    我个人将双亲委派机制理解成八个字:向上查找、向下委派。

    简单的画个图来解释一下:

image-20200603212046035.png

简单理解就是:一个类需要加载,会首先使用这个类的全命名不断的检查是否已经加载过。如果加载过就不允许加载,如果没有加载过那么就会尝试去加载,不过每一个类加载器都有自己固定的活动范围,所以找不到这个类的话就会委派给下一级的类加载器去加载,直到最后一级如果都加载不到,就会报classNotFoundException。

  1. 好了上面大体上解释了了什么是双亲委派机制,下面我们就需要扒开他的衣服好好看看里面

    首先了解几个类加载器:

    • Application ClassLoader:应用程序类加载器。这个类加载器是我们最为常见的,我们写的代码、从外部引入的类默认都是这个类加载器帮我们加载进去的
    • Extension ClassLoader:扩展类加载器。他就只负责加载**<JAVA_HOME>/lib/ext**文件夹下的类
    • Bootstrap ClassLoader:启动类加载器。他就更神秘了,他只负责加载**<JAVA_HOME>/lib/**文件夹下的类,而且他是c++实现的,我们还获取不到他。
    • ** ClassLoader:自定义类加载器。这个是我们自己实现的,可以指定加载某个类。只需要继承ClassLoader类,然后重写findClass方法就OK了。

    为什么需要双亲委派机制?

    为了安全。可以想象一个场景:你自己写一个java.lang.Stirng类,加载到项目中,然后在里面为所欲为,是不是就炸了。

    ​ 当然也有打破双亲委派机制的地方,比如说tomcat。它里面有很多歌application,如果不能同时加载,那就意味着一个tomcat只能跑一个项目了。

@Slf4j
public class MyClassLoader {

    public static void main(String[] args) {
        log.info("String 的 类加载器:" + String.class.getClassLoader());
        log.info("MyClassLoader 的 类加载器:" + MyClassLoader.class.getClassLoader());
        log.info("NashornBeansLinker 的 类加载器:" + NashornBeansLinker.class.getClassLoader());

        log.info("AppClassLoader 的 上级加载器:" +   MyClassLoader.class.getClassLoader().getParent());
        log.info("ExtClassLoader 的 上级加载器:" + NashornBeansLinker.class.getClassLoader().getParent());
    }
}

通过这个小程序我们也能够看出这些类加载器的不同。

当然也能看出一点,就是他们之间并不是父子关系,看到我日志里面的用词上级了没有,为了不与父加载器弄混淆。

下面我们简单的看一下加载类的源码就能清楚了:

 /**
     *
     * 使用指定的 二进制名称 加载类。此方法的 默认实现按以下的顺序搜索类:
     *
     * 1、调用 findLoadedClass(String) 以检查该类是否已加载。
     * 2、在父类加载器上调用 loadClass(String) loadClass 方法。如果父程序为 null ,则使用内置到虚拟机的类加载器。
     * 3、调用 findClass(String) 方法(当然这个findClass需要自己实现,默认的直接抛出ClassNotFoundException)查找该类。
     * 如果使用上述步骤找到了类,并且解析(resolve)标志为true,则此方法将在结果的 class 对象上调用 resurveClass(Class) 方法。
     *  「resurveClass方法其实就是将类进行链接,其实就是类加载过程的第二大步中的第三小步骤」
     *
     * 实现这个类的子类最好的是重写findClass(String) 而不是 这个方法,为什么呢?因为重写这个方法会打破双亲委派机制
     *
     * 除非重写,否则此方法在整个类加载过程中都是同步的
     *
     */
    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;
        }
	}

我简单的把注释给翻译了一遍,在这里不得不感慨一下,别人的源码的注释是真的好,看了注释之后源码就了解了大半。还有就是学习真的需要看源码。虽然一开始很痛苦,但是看懂了之后真的有很大益处。这边博客我就干了接近三天,都耗在源码上了。

好了不扯了,回到正题

注意上面源码中的两点:c = parent.loadClass(name, false);,这个就是不断的向上检查并加载的过程所在,也能看出来每个加载器之间并不是父子集成关系,而是在里面维护了一个引用:

image-20200606082421484.png

从上可以看到,ClassLoader初始化的时候就会将加载器的parent引用组装好了。

第二个就是需要注意的就是这个:

//这个类就是需要我们自己去实现的类,它定义好了钩子方法(设计模式中的模板方法模式) c = findClass(name);

也就是说我们可以实现自己的类加载器,然后重写findClass方法就可以了,话不多说,show the code:

我在桌面随便新建一个HelloWorlds.java ,里面只有一个main方法,打印hello world,然后将它编译成class文件。自己写一个加载器将它加载:

运行之后可以发现打印出来的类加载器就是我们自己写的MyClassLoader。

package com.example.studydemo.jvm;

/**
 * @program: studydemo
 * @description: jvm学习之类加载器(双亲委派机制)
 * @author: kby
 * @create: 2023-09-03 20:40
 */
@Slf4j
public class MyClassLoader extends ClassLoader {

    public static void main(String[] args) throws Exception {
        ClassLoader classLoader = new MyClassLoader();
        Class<?> aClass = classLoader.loadClass("HelloWorlds");
        System.out.println("HelloWorlds 的类加载器是:" + aClass.getClassLoader());
        Method main = aClass.getDeclaredMethod("main", String[].class);
        main.invoke(null,(Object) new String[0]);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        //找到class的绝对路径
        String fileName  = "/Users/mac/Desktop/"+name.replaceAll("\\.",File.separator)+".class";
        File f = new File(fileName);
        FileInputStream fileInputStream = null;
        ByteArrayOutputStream byteArrayOutputStream = null;
        try {
            fileInputStream = new FileInputStream(f);
            byte[] bytes = new byte[(int)f.length()];
            fileInputStream.read(bytes);//将文件读到字节数组中
            return defineClass(name, bytes, 0, bytes.length);//将字节数组转化成类
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (fileInputStream != null) {
                    fileInputStream.close();
                }
                if (byteArrayOutputStream != null) {
                    byteArrayOutputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return super.findClass(name);
    }

}

总结一下

今天我们主要讲类的双亲委派加载机制:

  1. 几种类加载器,以及他们加载的作用域
  2. 类加载器关于加载方法的源码
  3. 自己实现自己的类加载器