• Java 类加载机制与双亲委派


    Java 类加载机制与双亲委派

    Java 类加载机制

    其中loadClass的类加载过程有如下几步:

    在这里插入图片描述

    加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载

    • 加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的 main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的 java.lang.Class对象,作为方法区这个类的各种数据的访问入口
    • 验证:校验字节码文件的正确性
    • 准备:给类的静态变量分配内存,并赋予默认值
    • 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如 main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过 程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用,下 节课会讲到动态链接
    • 初始化:对类的静态变量初始化为指定的值,执行静态代码块类被加载到方法区中后主要包含 运行时常量池、类型信息、字段信息、方法信息、类加载器的 引用、对应class实例的引用等信息。 类加载器的引用:这个类到类加载器实例的引用 对应class实例的引用:类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的 对象实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。

    类在运行过程中如果使用到其它类,会逐步加载这些类。

    
    public class CalssLoadTest {
        static {
            System.out.println("*************load TestDynamicLoad************");
        }
    
        public static void main(String[] args) {
            new A();
            System.out.println("*************load test************");
            B b = null;
            //B不会加载,除非这里执行
            // new B();
    
        }
    
    }
    
    class A {
        static {
            System.out.println("*************load A************");
        }
    
        public A() {
            System.out.println("*************initial A************");
    
        }
    }
    
    class B {
        static {
            System.out.println("*************load B************");
        }
    
        public B() {
            System.out.println("*************initial B************");
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    用到那个类就加载那个类
    在这里插入图片描述

    类加载器

       //jdk自带核心类
            System.out.println(String.class.getClassLoader());
            //扩展类 ext包下
            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"));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    执行结果
    在这里插入图片描述

    可以看到

    sun.misc.Launcher E x t C l a s s L o a d e r s u n . m i s c . L a u n c h e r ExtClassLoader sun.misc.Launcher ExtClassLoadersun.misc.LauncherAppClassLoader

    可以看到默认的类加载器是appClassLoader, 父类设置了ExtClassLoader
    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    可以看到 AppClassLoader 和ExtClassLoader 父类 UrlClassLoader , 父类 ClassLoader
    在这里插入图片描述

    ExtClassLoader 父类 Null
    在这里插入图片描述

    Java里有如下几种类加载器

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

    双亲委派

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

    在这里插入图片描述

    可以看到这里首先判断类是否被加载过,有直接返回
    没有就先判断是否有父类加载器,有就父类加载器加载
    没有就bootstarp加载器加载
    还没有就叫给子加载器加载,加载到就返回
    在这里插入图片描述
    没有加载到依次子类加载

    在这里插入图片描述

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

    • 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心 API库被随意篡改

    • 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一 次,保证被加载类的唯一性

    自己定义一个 String 类,可以看到根本运行不了,甚至idea 编译代码这里 都没有出现run main 的标志

    在这里插入图片描述

    自定义类加载器

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

    package jvm;
    
    
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.IOException;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    
    public class MyClassLoader extends ClassLoader {
    
        String path;
        public MyClassLoader( String path){
            this.path=path;
        }
    
        private byte[] loadByte() throws Exception {
            ///Users/pilgrim/Desktop/User1.class
            FileInputStream fis = new FileInputStream(path);
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
    
        }
    
    
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
    
            byte[] data = new byte[0];
            try {
                data = loadByte();
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
            return defineClass(name, data, 0, data.length);
        }
    
    
        public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
    
    
            //初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载 器设置为应用程序类加载器AppClassLoader
            MyClassLoader classLoader = new MyClassLoader("/Users/pilgrim/Desktop/User1.class");
            //我们自己盘创建 jvm 目录,将User类的复制类User1.class丢入该目录
            Class clazz = classLoader.loadClass("jvm.User1");
            Object obj = clazz.newInstance();
            Method method = clazz.getDeclaredMethod("sout", null);
            method.setAccessible(true);
            method.invoke(obj);
            System.out.println(clazz.getClassLoader().getClass().getName());
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    结果
    注意一定要在自己目录把 User1的class文件删除了。不然就是app ClassLoader 去加载了。
    在这里插入图片描述

    不同版本的 User1 类文件

    这里将 User1 改一下 表示不同的版本
    在这里插入图片描述

    然后放到test目录下

    用两个类加载器来加载,加载执行的结果是第一个 User1 版本,类已经被加载过了就不会加载其他的版本了,类的全限定命名是一样的。

    在这里插入图片描述

    对于这种情况我们应该怎么办,写 2 个不同类名的 User1,User2。

    但是jar 依赖都是一个名,只是版本不一样,对此,我们需要打破这种委派。

    打破双亲委派机制

    如果是我们自己写的类 我们自己加载 不委派给父类。

    package jvm.myclassloader;
    
    
    import java.io.FileInputStream;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    
    /**
     * 打破双亲委派 *
     */
    public class MyClassLoader2 extends ClassLoader {
    
        String path;
    
        public MyClassLoader2(String path) {
            this.path = path;
        }
    
        private byte[] loadByte() throws Exception {
            ///Users/pilgrim/Desktop/User1.class
            FileInputStream fis = new FileInputStream(path);
            int len = fis.available();
            byte[] data = new byte[len];
            fis.read(data);
            fis.close();
            return data;
    
        }
    
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            synchronized (getClassLoadingLock(name)) {
                // First, check if the class has already been loaded
                Class<?> c = findLoadedClass(name);
                if (c == null) {
                    //是我们自己的类 自己加载 不是让父类加载
                    if (name.startsWith("jvm")) {
                        c = findClass(name);
                    } else {
                        c = this.getParent().loadClass(name);
                    }
                }
                return c;
            }
        }
    
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
    
            byte[] data = new byte[0];
            try {
                data = loadByte();
            } catch (Exception e) {
                e.printStackTrace();
                throw new ClassNotFoundException();
            }
            return defineClass(name, data, 0, data.length);
        }
    
    
        public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
    
    
            MyClassLoader2 classLoader1 = new MyClassLoader2("/Users/pilgrim/Desktop/User1.class");
            //我们自己盘创建 jvm 目录,将User类的复制类User1.class丢入该目录
            Class clazz1 = classLoader1.loadClass("jvm.User1");
            Object obj1 = clazz1.newInstance();
            Method method1 = clazz1.getDeclaredMethod("sout", null);
            method1.setAccessible(true);
            method1.invoke(obj1);
            System.out.println(clazz1.getClassLoader().getClass().getName());
    
    
            //初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载 器设置为应用程序类加载器AppClassLoader
            MyClassLoader2 classLoader2 = new MyClassLoader2("/Users/pilgrim/Desktop/test/User1.class");
            //我们自己盘创建 jvm 目录,将User类的复制类User1.class丢入该目录
            Class clazz2 = classLoader2.loadClass("jvm.User1");
            Object obj2 = clazz2.newInstance();
            Method method = clazz2.getDeclaredMethod("sout", null);
            method.setAccessible(true);
            method.invoke(obj2);
            System.out.println(clazz2.getClassLoader().getClass().getName());
    
    
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87

    可以看到,不同的类加载器加载执行的类结果是不一样的,也就是说不同的版本的类,相同的名,被分别加载了。

    在这里插入图片描述

    同一个JVM内,两个相同包名和类名的类对象可以共存,因为他们的类加载器可以不一 样,所以看两个类对象是否是同一个,除了看类的包名和类名是否都相同之外,还需要他们的类 加载器也是同一个才能认为他们是同一个。

    Tomcat打破双亲委派机制

    其实这种就是 Tomcat 类加载器的原型

    • 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的 不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是 独立的,保证相互隔离。

    • 如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认 的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。 默认的类加载器是能够实现的,因为他的职责就是保证唯一性

    • jsp文件的热加载,可以直接卸载掉这jsp文件的类加载器,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载 器。重新创建类加载器,重新加载jsp文件。

    在这里插入图片描述

    tomcat的几个主要类加载器:

    • commonLoader:Tomcat最基本的类加载器,加载路径中的class可以被Tomcat容 器本身以及各个Webapp访问;

    • catalinaLoader:Tomcat容器私有的类加载器,加载路径中的class对于Webapp不 可见;

    • sharedLoader:各个Webapp共享的类加载器,加载路径中的class对于所有 Webapp可见,但是对于Tomcat容器不可见;

    • WebappClassLoader:各个Webapp私有的类加载器,加载路径中的class只对当前 Webapp可见,比如加载war包里相关的类,每个war包应用都有自己的

    • WebappClassLoader,实现相互隔离,比如不同war包应用引入了不同的spring版本, 这样实现就能加载各自的spring版本;

    每个 webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器,打破了双亲委 派机制。

  • 相关阅读:
    助力物流业发展2024长三角快递物流供应链与技术装备展于四月召开
    消息队列中,如何保证消息的顺序性?
    EMQX安装与使用文档
    如何让 ABAP 报表在后台作业的模式下运行试读版
    SWT/ANR问题--ANR/JE引发SWT
    fcpx插件:82种复古电影胶卷框架和效果mFilm Matte
    VSCode远程连接
    Go并发的非阻塞缓存
    数据结构详细笔记——线性表
    【计算机网络】数据链路层:使用点对点信道的数据链路层
  • 原文地址:https://blog.csdn.net/weixin_38361347/article/details/127760950