JVM学习笔记02-类加载器和双亲委派机制

270 阅读3分钟

Java中的类加载器

  • 引导类加载器:负责加载位于JRE的lib目录下的核心类库,比如rt.jarcharsets.jar
  • 扩展类加载器:负责加载位于JRE的lib目录下的ext扩展目录中的jar包的类
  • 应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载开发人员编写的类
  • 自定义加载器:负责加载用户自定义路径下的类包

下面这段代码将打印出上述几种情况中,各个类对应的类加载器

package org.laugen.jvm;

import sun.misc.Launcher;
import java.net.URL;

public class JdkClassLoader {
    public static void main(String[] args) {
        System.out.println(String.class.getClassLoader());
        System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader().getClass().getName());
        System.out.println(JdkClassLoader.class.getClassLoader().getClass().getName());
        System.out.println("=============================================");
        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
        ClassLoader extClassloader = appClassLoader.getParent();
        ClassLoader bootstrapLoader = extClassloader.getParent();
        System.out.println("the bootstrapLoader : " + bootstrapLoader);
        System.out.println("the extClassloader : " + extClassloader);
        System.out.println("the appClassLoader : " + appClassLoader);
        System.out.println("=============================================");
        System.out.println("bootstrapLoader加载以下文件:");
        URL[] urls = Launcher.getBootstrapClassPath().getURLs();
        for (int i = 0; i < urls.length; i++) {
            System.out.println(urls[i]);
        }
        System.out.println("=============================================");
        System.out.println("extClassloader加载以下文件:");
        System.out.println(System.getProperty("java.ext.dirs"));
        System.out.println("=============================================");
        System.out.println("appClassLoader加载以下文件:");
        System.out.println(System.getProperty("java.class.path"));
    }
}

执行结果如下:

    null
    sun.misc.Launcher$ExtClassLoader
    sun.misc.Launcher$AppClassLoader
    =============================================
    the bootstrapLoader : null
    the extClassloader : sun.misc.Launcher$ExtClassLoader@4b67cf4d
    the appClassLoader : sun.misc.Launcher$AppClassLoader@18b4aac2
    =============================================
    bootstrapLoader加载以下文件:
    file:/D:/Java/jdk1.8.0_191/jre/lib/resources.jar
    file:/D:/Java/jdk1.8.0_191/jre/lib/rt.jar
    file:/D:/Java/jdk1.8.0_191/jre/lib/sunrsasign.jar
    file:/D:/Java/jdk1.8.0_191/jre/lib/jsse.jar
    file:/D:/Java/jdk1.8.0_191/jre/lib/jce.jar
    file:/D:/Java/jdk1.8.0_191/jre/lib/charsets.jar
    file:/D:/Java/jdk1.8.0_191/jre/lib/jfr.jar
    file:/D:/Java/jdk1.8.0_191/jre/classes
    =============================================
    extClassloader加载以下文件:
    D:\Java\jdk1.8.0_191\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
    =============================================
    appClassLoader加载以下文件:D:\Java\jdk1.8.0_191\jre\lib\charsets.jar;D:\Java\jdk1.8.0_191\jre\lib\deploy.jar;D:\Java\jdk1.8.0_191\jre\lib\ext\access-bridge-64.jar;D:\Java\jdk1.8.0_191\jre\lib\ext\cldrdata.jar;D:\Java\jdk1.8.0_191\jre\lib\ext\dnsns.jar;D:\Java\jdk1.8.0_191\jre\lib\ext\jaccess.jar;D:\Java\jdk1.8.0_191\jre\lib\ext\jfxrt.jar;D:\Java\jdk1.8.0_191\jre\lib\ext\localedata.jar;D:\Java\jdk1.8.0_191\jre\lib\ext\nashorn.jar;D:\Java\jdk1.8.0_191\jre\lib\ext\sunec.jar;D:\Java\jdk1.8.0_191\jre\lib\ext\sunjce_provider.jar;D:\Java\jdk1.8.0_191\jre\lib\ext\sunmscapi.jar;D:\Java\jdk1.8.0_191\jre\lib\ext\sunpkcs11.jar;D:\Java\jdk1.8.0_191\jre\lib\ext\zipfs.jar;D:\Java\jdk1.8.0_191\jre\lib\javaws.jar;D:\Java\jdk1.8.0_191\jre\lib\jce.jar;D:\Java\jdk1.8.0_191\jre\lib\jfr.jar;D:\Java\jdk1.8.0_191\jre\lib\jfxswt.jar;D:\Java\jdk1.8.0_191\jre\lib\jsse.jar;D:\Java\jdk1.8.0_191\jre\lib\management-agent.jar;D:\Java\jdk1.8.0_191\jre\lib\plugin.jar;D:\Java\jdk1.8.0_191\jre\lib\resources.jar;D:\Java\jdk1.8.0_191\jre\lib\rt.jar;E:\learning\boat\jvm\code\jvm\target\classes;D:\JetBrains\IntelliJ IDEA 2020.3\lib\idea_rt.jar

注意:由于引导类加载器BootstrapClassLoader的实例是由C++创建的,因此这里打印出来的是null

类加载器初始化过程

创建JVM启动器实例sun.misc.Launcher时,在起构造方法Launcher()内部,会创建扩展类加载器ExtClassLoader和应用程序类加载器AppClassLoader

JVM会默认调用LaunchergetClassLoader()方法返回的类加载器加载我们编写的类,而它返回的正是AppClassLoader

public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        // 构造拓展类加载器,过程中会将其parent设置为null
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
        // 构造应用程序类加载器,起parent设置为ExtClassLoader
        // Launcher的loader属性值时AppClassLoader,一般用这个加载器加载我们的类
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }

    Thread.currentThread().setContextClassLoader(this.loader);
    String var2 = System.getProperty("java.security.manager");
    // 省略代码......
}

双亲委派机制

类加载器.png

如图所示,就是双亲委派机制的核心体现,一句话概括就是,先找父加载器加载,不行再由子加载器自己加载

这里通过阅读AppClassLoader加载类的源码来理解一下双亲委派机制,当我们调用AppClassLoaderloadClass方法时,最终实际调用的是其父类ClassLoaderloadClass方法,该方法的大体流程如下:

  1. 检查一下该类是否已经加载过了,如果加载过了,就直接返回
  2. 如果该类没有加载过,判断当前加载器是否由父加载器,如若有,则由父加载器加载,如若无,则由引导类加载器加载
  3. 如过父加载器和引导类加载器都没有找到指定的类,则由当前类加载器的findClass方法完成类加载
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 先检查该类是否已经加载过了
        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();
                // 调用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;
    }
}

protected Class<?> findClass(final String name) throws ClassNotFoundException {
    final Class<?> result;
    try {
        result = AccessController.doPrivileged(
            new PrivilegedExceptionAction<Class<?>>() {
                public Class<?> run() throws ClassNotFoundException {
                    String path = name.replace('.', '/').concat(".class");
                    Resource res = ucp.getResource(path, false);
                    if (res != null) {
                        try {
                            // defineClass方法会执行一系列类加载的过程
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        }
                    } else {
                        return null;
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
    if (result == null) {
        throw new ClassNotFoundException(name);
    }
    return result;
}

为什么要设计双亲委派机制?

  • 沙箱安全机制:用户不能自己实现核心类库中的类,以此防止核心API库被随意篡改
  • 避免类的重复加载:当父加载器已经加载了该类时,子加载器就没有必要再加载一次了,保证被加载类的唯一性

全盘负责委托机制

全盘负责是指当一个ClassLoader装在一个类时,除非显式地使用另一个ClassLoader,否则该类所依赖及引用的类默认由这个ClassLoader载入

自定义类加载器

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

package org.laugen.jvm;

public class Note {
    static {
        System.out.println("加载了org.laugen.jvm.Note类");
    }
    
    public Note() {
        System.out.println("创建了org.laugen.jvm.Note类的实例");
    }
    
    public void print() {
        System.out.println("这是一个note");
    }
}
package org.laugen.jvm;

import java.io.FileInputStream;
import java.lang.reflect.Method;

public class TestCustomizeClassLoader {
    static class CustomizeClassLoader extends ClassLoader {
        private String classPath;

        public CustomizeClassLoader(String classPath) {
            this.classPath = classPath;
        }

        // 读取class字节码文件
        private byte[] loadByte(String name) throws Exception {
            name = name.replaceAll("\\.", "/");
            FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
        }

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] data = loadByte(name);
                return defineClass(name, data, 0, data.length);
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
        }

    }

    public static void main(String args[]) throws Exception {
        CustomizeClassLoader classLoader = new CustomizeClassLoader("D:/MyClasses");
        System.out.println("自定义类加载器的父加载器:" + classLoader.getParent().getClass().getName());
        Class clazz = classLoader.loadClass("org.laugen.jvm.Note");
        Object obj = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("print", null);
        // 调用Note类的print方法
        method.invoke(obj, null);
        System.out.println("Note类的类加载器是:" + clazz.getClassLoader().getClass().getName());
    }
}

执行结果如下:

自定义类加载器的父加载器:sun.misc.Launcher$AppClassLoader
加载了org.laugen.jvm.Note类
创建了org.laugen.jvm.Note类的实例
这是一个note
Note类的类加载器是:org.laugen.jvm.TestCustomizeClassLoader$CustomizeClassLoader

打破双亲委派机制

双亲委派机制的核心实现就在loadClass方法中,因此,打破双亲委派机制,需要重写loadClass方法

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t1 = System.nanoTime();
            if (name.startsWith("org.laugen.jvm.Note")) {
                // Note类加载的时候不走双亲委派过程,直接加载
                c = findClass(name);
            } else {
                // 由于加载Note类时,需要加载Object,因为全盘委托设计,此处会用我们自定义的加载器加载
                // 因此,针对这些类,应该交由父加载器AppClassLoader去走双亲委派机制完成加载
                c = this.getParent().loadClass(name);
            }
            PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
            PerfCounter.getFindClasses().increment();
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

那么,你见过哪些打破双亲委派机制的设计呢?