• jvm双亲委派机制详解


    双亲委派机制

    ​ 记录一下JVM的双亲委派机制学习记录。

    类加载器种类

    ​ 当我们运行某一个java类的main方法时,首先需要由java虚拟机的类加载器将我们要执行的main方法所在的class文件加载到jvm中,这里提到的类加载器大概有4种:

    引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如rt.jar、charsets.jar等
    扩展类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包
    应用程序类加载器:负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类
    自定义加载器:负责加载用户自定义路径下的类包。

    ​ 每个类加载器加载的包路径都是不同的,有各自的职责。通过一下示例,可以看出每个类加载器加载的路径:

    public class TestJDKClassLoader {
    
        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(TestJDKClassLoader.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@330bedb4
    the appClassLoader : sun.misc.Launcher$AppClassLoader@14dad5dc
    
    bootstrapLoader加载以下文件:
    file:/D:/ProgramFiles/jdk1.8.0_45_64bit/jre/lib/resources.jar
    file:/D:/ProgramFiles/jdk1.8.0_45_64bit/jre/lib/rt.jar
    file:/D:/ProgramFiles/jdk1.8.0_45_64bit/jre/lib/sunrsasign.jar
    file:/D:/ProgramFiles/jdk1.8.0_45_64bit/jre/lib/jsse.jar
    file:/D:/ProgramFiles/jdk1.8.0_45_64bit/jre/lib/jce.jar
    file:/D:/ProgramFiles/jdk1.8.0_45_64bit/jre/lib/charsets.jar
    file:/D:/ProgramFiles/jdk1.8.0_45_64bit/jre/lib/jfr.jar
    file:/D:/ProgramFiles/jdk1.8.0_45_64bit/jre/classes
    
    extClassloader加载以下文件:
    D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
    
    appClassLoader加载以下文件:
    D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\charsets.jar;D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\deploy.jar;D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\ext\access-bridge-64.jar;D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\ext\cldrdata.jar;D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\ext\dnsns.jar;D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\ext\jaccess.jar;D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\ext\jfxrt.jar;D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\ext\localedata.jar;D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\ext\nashorn.jar;D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\ext\sunec.jar;D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\ext\sunjce_provider.jar;D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\ext\sunmscapi.jar;D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\ext\sunpkcs11.jar;D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\ext\zipfs.jar;D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\javaws.jar;D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\jce.jar;D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\jfr.jar;D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\jfxswt.jar;D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\jsse.jar;D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\management-agent.jar;D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\plugin.jar;D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\resources.jar;D:\ProgramFiles\jdk1.8.0_45_64bit\jre\lib\rt.jar;D:\Files\learn\tuling\jvm-demo\target\classes;D:\ProgramFiles\ideaIU-2021.3.3.win\lib\idea_rt.jar
    
    Process finished with exit code 0
    

    appClassLoader虽然打印的内容虽然很多,但它只需要加载target目录下的文件。

    双亲委派机制

    ​ 虽然基本只有4种类加载器,但这4种类加载器之间是存在一定的关联关系的。如下图:

    ​ 加载某个类时会先委托给父加载器寻找目标类,找不到再委托上层父类加载器加载,所有的父类加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中寻找并载入目标类。

    ​ 比如上面的TestJDKClassLoader,首先会委托应用程序类加载,应用程序类加载器则委托扩展类加载器加载,扩展类加载器则委托引导类加载器加载,引导类加载器在它的类加载路径下找不到TestJDKClassLoader.class文件,则向下委托扩展类加载器加载,扩展类加载器加载不到则委托应用程序类加载器自己加载,于是应用程序类加载器在target目录下找到并载入了TestJDKClassLoader.class文件。

    我们来看下应用程序类加载器AppClassLoader加载类的双亲委派机制源码,AppClassLoader的loadClass方法最终会调用其父类ClassLoader的loadClass方法,该方法的大体逻辑如下:

    我们来看下应用程序类加载器AppClassLoader加载类的双亲委派机制源码,AppClassLoader的loadClass方法最终会调用其父类ClassLoader的loadClass方法,该方法的大体逻辑如下:

    我们来看下应用程序类加载器AppClassLoader加载类的双亲委派机制源码,AppClassLoader的loadClass方法最终会调用其父类ClassLoader的loadClass方法,该方法的大体逻辑如下:

    1. 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接
      返回。

    2. 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加
      载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加
      载。

    3. 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的
      findClass方法来完成类加载。

    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;
        }
    }
    

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

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

    例子:比如我们自己新建了一个java.lang.String类,我们看能不能加载成功。

    package java.lang;
    
    public class String {
    
        public static void main(String[] args) {
            System.out.println("============自己的类加载器====");
        }
    }
    
    // 执行结果
    错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
       public static void main(String[] args)
    否则 JavaFX 应用程序类必须扩展javafx.application.Application
    

    自定义类加载器

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

    public class MyClassLoaderTest {
        static class MyClassLoader extends ClassLoader {
            private String classPath;
    
            public MyClassLoader(String classPath) {
                this.classPath = classPath;
            }
    
            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;
            }
    
            protected Class findClass(String name) throws ClassNotFoundException {
                try {
                    byte[] data = loadByte(name);
                    //defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。
                    return defineClass(name, data, 0, data.length);
                } catch (Exception e) {
                    e.printStackTrace();
                    throw new ClassNotFoundException();
                }
            }
        }
    
        public static void main(String[] args) throws Exception {
            //初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
            MyClassLoader classLoader = new MyClassLoader("D:/test");
            //D盘创建 test/com/hyz/jvm 几级目录,将User类的复制类User1.class丢入该目录
            Class clazz = classLoader.loadClass("com.hyz.jvm.User1");
    //        Class clazz = classLoader.loadClass("java.lang.String");
            Object obj = clazz.newInstance();
            Method method = clazz.getDeclaredMethod("hello", null);
            method.invoke(obj, null);
    
            System.out.println(clazz.getClassLoader().getClass().getName());
        }
    }
    

    打破双亲委派机制

    ​ 假如我们的target下有一个User类,但是我们的程序代码中需要去读取D:/test/com/hyz/jvm/User.class类,根据双亲委派机制,肯定是会加载到target下的User类的,要如何才能加载到D:/test下的User类呢?

    ​ 那意味着我们需要去打破双亲委派机制。看AppClassLoader的类加载逻辑,主要逻辑在父类ClassLoader.loadClass()方法中,我们只需要在自定义的类加载器中重写该方法即可。主要修改逻辑:如果类型是com.hyz.jvm开头的类,则从自定义类加载器中去读取,否则委托给上层类加载器加载。

    /**
     * 32 * 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
     * 33 * @param name
     * 34 * @param resolve
     * 35 * @return
     * 36 * @throws ClassNotFoundException
     * 37
     */
    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) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                if (!name.startsWith("com.hyz.jvm")) {
                    c = this.getParent().loadClass(name);
                } else {
                    c = findClass(name);
                }
                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
    

    ​ 应用到打破双亲委派机制的实际应用场景是在Tomcat加载war包。比如war1用的是spring4版本,war2用的是spring5版本,那就意味着加载着2个war包不能用同一个类加载器实例,需要各自指定一个自定义的类加载器实例,各自去加载所需的spring版本库文件。

    总结:双亲委派机制保证了核心类的安全,确保不会被修改,也保证了不会加载到重复的字节码文件。

  • 相关阅读:
    深入浅出:SPI机制在JDK与Spring Boot中的应用
    【每日一题】318. 最大单词长度乘积-2023.11.6
    【一包通刷】晶晨S905L3A/B_完美AI语音线刷包_打开ADB_ROOT权限
    网易秋招高频面试题汇总
    Mysql压缩包安装过程细节说明
    Uni-app智慧工地可视化信息平台源码
    离子液体1-乙基-3-甲基咪唑六氟磷酸盐([EMIm][PF6])修饰纳米Fe3O4四氧化三铁(规格)
    《FFmpeg Basics》中文版-08-模糊,锐化和其他去噪
    php权限调整强制用户退出的解决方案
    微内核操作系统
  • 原文地址:https://www.cnblogs.com/process-h/p/16867624.html