✨使用了Java这么久,你了解类加载时的双亲委派和类加载器吗?

230 阅读5分钟

前言

说到"双亲委派"使用Java开发的同鞋我想都应该听说过这个东西。在了解双亲委派前应该先熟悉Java的类加载(包括:类加载的机制、类加载器、类加载器的层级关系)。

类加载器

在Java中存在两种类加载器引导类加载器(BootStrapClassLoader)和自定义类加载器(这里说的自定义类加载器包括扩展类加载器(ExtClassLoader)和应用类加载器(AppClassLoader))。

Tips:在JVM规范中将所有派生自ClassLoader类的的所有子类都统称为自定义类加载器。

在JDK8中有如下三种类加载器:

  1. 引导类加载器(BootStrapClassLoader)
  2. 扩展类加载器(ExtClassLoader)
  3. 应用类加载器(AppClassLoader)

引导类加载器

引导类加载器是在使用java.exe启动一个Java程序的时候由C++代码创建并执行的一个加载类的程序,它主要加载的是在jdk/lib文件夹中的jar包或者是通过-XBootClassPath所指定的路径中的jar包。这个文件夹中的jar包是在程序运行时的核心包。

扩展类加载器

扩展类加载器主要加载的是在jdk/ext目录中的jar包或者是在系统变量中通过java.ext.dirs所指定的目录。

应用类加载器

应用类加载器是加载的classPath中的jar或都是class文件。比如说写一个项目这个项目中有很多我们自已创建的Java类,应用类加载器更多的是加载这些我们自己创建的类生成的class文件

双亲委派

双亲委派的工作机制:当JVM接收到了加载类的请求时,自已不会去加载这个类,而是先委托给父加载器进行加载,如果父加载器已经加载过这个类那么就会直接的返回,如果没有加载过这个类就再次往上委托,让他的父加载器进行加载。如果顶层的父加载器还是没有加载到这个类,就会向下委托,让子加载器进行加载这个类。

加载器委托的顺序:AppClassLoader -> ExtClassLoader -> BootStrapClassLoader

image.png

到这里可能有些同学会有疑问?

问:为什么要在最底层的类加载器向上委托而不是直接在顶层的类加载器直接向下委托呢?

答:在我们做的项目,95%的Jar包都是我们自已创建的Jar包,如果每次加载类的时候都是在顶层向下委托那么每次加载类都是一个很慢的过程,如果是在最底层向上委托只有第一次加载类的时候会比较慢。

使用双亲委派类加载模式后,他还会有一个比较好的功能,程序员无法自已恶意修改核心类库,比如:我自已在项目中创建了一个java.lang.String的类,由于双亲委派的加载机制,我自已创建的这个String类并不会加载到JVM中,加载到JVM中的String类还是JDK提供的String。

这个问题的原因还是要归属到JVM中的类加载机制,上面我们说过,在加载类的时候先是向上委托经过AppClassLoader -> ExtClassLoader -> BootStrapClassLoader的层层委托,当委托到了BootStrapClassLoader后在顶层的引导类加载器中就已经可以把String类加载到了JVM中。所以BootStrapClassLoader并不会让子加载器去加载器我自已创建的那个String类。

双亲委派源码

向上委托的源码主要是体现在ClassLoader#loadClass类中。

比如:举例这段代码是AppClassLoader执行的代码(其实AppClassLoader和ExtClassLoader都会执行下面段代码)。

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 先是检测当前类加载器有没有加载过个类,第一次加载这个指定是null,所以就进入了下面if分支
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                //这一步就是上面所说的向上委托,如果当前加载器是AppClassLoader,这里的parent就是ExtClassLoader,如果当前的加载器是ExtClassLoader 就会走到else分支。
                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 (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

Tips:类加载器的关系是父子关系,而不是类逻辑上的继承的父子关系

自定义类加载器

在上文中也提到过自定义类加载器,为什么要有自定义类加载器?

比如:有的class文件需要在网络上获取,还有的在加载class时需要对其进行解密处理。这些功能都需要自定义类加载器,最为典型的自定义类加载器就是Tomcat的自定义类加载器打破双亲委派机制。

比如我现在要实现一个从网络上加载class文件的功能,通过下面这张图显示子类加载器最终需要显示的是ClassLoader抽象类。在自定义加载器的时候也是只需要实现这个类就好,重写findClass方法

image.png

static class CustomClassLoader extends ClassLoader{
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        return super.findClass(name);
   }
}

而在加载类的过程只需要修改findClass的代码即可。如果需要打破双亲委派还需要重写loadClass方法。

比如:现在在一个nginx服务器上有一个com.yqs.User的类,这个类中有一个p方法,我需要将其从网络上加载后执行p方法。应该怎么做呢?

static class CustomClassLoader extends ClassLoader{

    /**
     * 从网络上将这个Class文件下载后转成字节数组
     **/
    public byte[] getByte(){
        byte[] p = null;
        try{
            URL url = new URL("http://localhost:8081/User.class");
            URLConnection conn = url.openConnection();
            InputStream is = conn.getInputStream();
            p = convert2ByteArray(is);
        }catch (MalformedURLException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        return p;
    }
    
    /**
     * 将输入的输入流转换成字节数组 
     **/
    public byte[] convert2ByteArray(InputStream inputStream) throws IOException{
        ByteArrayOutputStream swap = new ByteArrayOutputStream();
        byte[] buff = new byte[1024];
        int rc = 0;
        while ((rc = inputStream.read(buff,0,1024)) > 0 ){
            swap.write(buff,0,rc);
        }
        byte[] in_b = swap.toByteArray();
        return in_b;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] aByte = getByte();
        //通过defineClass方法将字节数组转成Class实例
        return defineClass(name,aByte,0,aByte.length);
    }
}

测试代码:

public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
    CustomClassLoader customClassLoader = new CustomClassLoader();
    Class<?> aClass = customClassLoader.loadClass("com.yqs.User");
    Object o = aClass.newInstance();
    Method p = aClass.getDeclaredMethod("p", null);
    p.invoke(o,null);
}

image.png

最后

这些就是近期整理的双亲委派和类加载器的一些知识,希望对你们有所帮助。