• JVM类加载机制详解


    JVM类加载运行全过程

    运行Math类的main函数,启动程序时,首先需要通过类加载器把类加载到JVM。

    package com.cold;
    
    public class Math {
    
        public int compute() {
            int a = 1;
            int b = 2;
            int c = (a + b) * 10;
            return c;
        }
    
        public static void main(String[] args) {
            Math math = new Math();
            math.compute();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述

    loadClass的类加载

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

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

    • 加载: 在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
    • 验证: 校验字节码文件的正确性
    • 准备: 给类的静态变量分配内存,并赋予默认值。
    • 解析: 将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法()替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用。
    • 初始化: 对类的静态变量初始化为指定的值,执行静态代码块。

    类被加载到方法区中的主要信息: 运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应class实例的引用。

    类加载器的引用: 这个类到类加载器实例的引用

    对应class实例的引用: 类加载器在加载类信息放到方法区中后,会创建一个对应的Class 类型的对象实例放到堆(Heap)中, 作为开发人员访问方法区中类定义的入口和切入点。

    类加载属于懒加载,程序运行时使用到什么类就会加载什么类。

    类加载器和双亲委派机制

    类加载过程主要是通过类加载器来实现的,Java中有以下几种类加载器:

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

    类加载示例

    package com.cold;
    
    import sun.misc.Launcher;
    
    import java.net.URL;
    
    public class TestClassLoader {
    
        public static void main(String[] args) {
            System.out.println(String.class.getClassLoader());
            System.out.println(com.sun.crypto.provider.DESKeyFactory.class.getClassLoader());
            System.out.println(TestClassLoader.class.getClassLoader());
    
            System.out.println();
    
            ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
            ClassLoader extClassLoader = appClassLoader.getParent();
            ClassLoader bootstrapClassLoader = extClassLoader.getParent();
    
            System.out.println("the bootstrapClassLoader : " + bootstrapClassLoader);
            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
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    输出结果

    null
    sun.misc.Launcher$ExtClassLoader@77459877
    sun.misc.Launcher$AppClassLoader@18b4aac2
    
    the bootstrapClassLoader : null
    the extClassLoader : sun.misc.Launcher$ExtClassLoader@77459877
    the appClassLoader : sun.misc.Launcher$AppClassLoader@18b4aac2
    
    bootstrapLoader加载以下文件:
    file:/D:/Program%20Files/Java/jdk-1.8/jre/lib/resources.jar
    file:/D:/Program%20Files/Java/jdk-1.8/jre/lib/rt.jar
    file:/D:/Program%20Files/Java/jdk-1.8/jre/lib/jsse.jar
    file:/D:/Program%20Files/Java/jdk-1.8/jre/lib/jce.jar
    file:/D:/Program%20Files/Java/jdk-1.8/jre/lib/charsets.jar
    file:/D:/Program%20Files/Java/jdk-1.8/jre/lib/jfr.jar
    file:/D:/Program%20Files/Java/jdk-1.8/jre/classes
    
    extClassloader加载以下文件:
    D:\Program Files\Java\jdk-1.8\jre\lib\ext;C:\Windows\Sun\Java\lib\ext
    
    appClassLoader加载以下文件:
    D:\Program Files\Java\jdk-1.8\jre\lib\charsets.jar;D:\Program Files\Java\jdk-1.8\jre\lib\deploy.jar;D:\Program Files\Java\jdk-1.8\jre\lib\ext\access-bridge-64.jar;D:\Program Files\Java\jdk-1.8\jre\lib\ext\cldrdata.jar;D:\Program Files\Java\jdk-1.8\jre\lib\ext\dnsns.jar;D:\Program Files\Java\jdk-1.8\jre\lib\ext\jaccess.jar;D:\Program Files\Java\jdk-1.8\jre\lib\ext\jfxrt.jar;D:\Program Files\Java\jdk-1.8\jre\lib\ext\localedata.jar;D:\Program Files\Java\jdk-1.8\jre\lib\ext\nashorn.jar;D:\Program Files\Java\jdk-1.8\jre\lib\ext\sunec.jar;D:\Program Files\Java\jdk-1.8\jre\lib\ext\sunjce_provider.jar;D:\Program Files\Java\jdk-1.8\jre\lib\ext\sunmscapi.jar;D:\Program Files\Java\jdk-1.8\jre\lib\ext\sunpkcs11.jar;D:\Program Files\Java\jdk-1.8\jre\lib\ext\zipfs.jar;D:\Program Files\Java\jdk-1.8\jre\lib\javaws.jar;D:\Program Files\Java\jdk-1.8\jre\lib\jce.jar;D:\Program Files\Java\jdk-1.8\jre\lib\jfr.jar;D:\Program Files\Java\jdk-1.8\jre\lib\jfxswt.jar;D:\Program Files\Java\jdk-1.8\jre\lib\jsse.jar;D:\Program Files\Java\jdk-1.8\jre\lib\management-agent.jar;D:\Program Files\Java\jdk-1.8\jre\lib\plugin.jar;D:\Program Files\Java\jdk-1.8\jre\lib\resources.jar;D:\Program Files\Java\jdk-1.8\jre\lib\rt.jar;E:\working_folder\code\test-project\target\classes;D:\Program Files\JetBrains\IntelliJ IDEA 2023.1.1\lib\idea_rt.jar
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    类加载器初始化过程:

    参考类运行加载全过程图,可知其中会创建JVM启动器实例sun.misc.Launcher

    在Launcher构造方法内部,其创建了两个类加载器,分别是sun.misc.Launcher.ExtClassLoader(扩展类加载器)和sun.misc.Launcher.AppClassLoader(应用类加载器)。

    JVM默认使用Launcher的getClassLoader()方法返回的类加载器AppClassLoader的实例加载我们的应用程序。

    在这里插入图片描述

    双亲委派机制

    这里类加载其实就有一个双亲委派机制,加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。

    比如我们的Math类,最先会找应用程序类加载器加载,应用程序类加载器会先委托扩展类加载器加载,扩展类加载器再委托引导类加载器,顶层引导类加载器在自己的类加载路径里找了半天没找到Math类,则向下退回加载Math类的请求,扩展类加载器收到回复就自己加载,在自己的类加载路径里找了半天也没找到Math类,又向下退回Math类的加载请求给应用程序类加载器,应用程序类加载器于是在自己的类加载路径里找Math类,结果找到了就自己加载了。

    在这里插入图片描述

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

    1. 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
    2. 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用bootstrap类加载器来加载。
    3. 如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类加载器的findClass方法来完成类加载。

    在这里插入图片描述

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

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

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

    全盘负责委托机制

    “全盘负责”是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类也由这个ClassLoder载入。

    自定义类加载器示例

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

    package com.cold;
    
    import java.io.FileInputStream;
    import java.io.IOException;
    
    public class MyClassLoaderTest {
    
        static class MyClassLoader extends ClassLoader{
            private String classPath;
    
            public MyClassLoader(String classPath){
                this.classPath = classPath;
            }
    
            private byte[] loadByte(String name) throws IOException {
                name = name.replaceAll("\\.", "/");
                FileInputStream is = new FileInputStream(classPath+"/"+name+".class");
                int len = is.available();
                byte[] data = new byte[len];
                is.read(data);
                is.close();
                return data;
            }
    
            @Override
            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("E:/test");
            //复制User.class文件到D:/test/com/cold目录下
            Class<?> clazz = classLoader.loadClass("com.cold.User");
            // 删除原有的User类输出的结果为com.cold.MyClassLoaderTest$MyClassLoader
            // 不删除原有的User类输出的结果为sun.misc.Launcher$AppClassLoader
            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

    Tomcat打破双亲委派机制

    以Tomcat类加载为例,Tomcat 如果使用默认的双亲委派类加载机制行不行?

    我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:

    1. 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
    2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。
    3. web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
    4. web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情, web容器需要支持 jsp 修改后不用重启。

    再看看我们的问题:Tomcat 如果使用默认的双亲委派类加载机制行不行?

    答案是不行的。为什么?

    第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。

    第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。

    第三个问题和第一个问题一样。

    我们再看第四个问题,我们想我们要怎么实现jsp文件的热加载,jsp 文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。

    Tomcat自定义加载器详解

    在这里插入图片描述

    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版本;

    从图中的委派关系中可以看出:

    CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。

    WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。

    而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的热加载功能。

    模拟实现Tomcat的webappClassLoader加载自己war包应用内不同版本类实现相互共存与隔离

    只需重写Class loadClass(String name, boolean resolve),实现自己的加载逻辑,不委派给双亲加载。

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

    package com.cold;
    
    import java.io.FileInputStream;
    import java.io.IOException;
    
    public class MyClassLoaderTest {
    
    
        static class MyClassLoader extends ClassLoader{
            private String classPath;
    
            public MyClassLoader(String classPath){
                this.classPath = classPath;
            }
    
            private byte[] loadByte(String name) throws IOException {
                name = name.replaceAll("\\.", "/");
                FileInputStream is = new FileInputStream(classPath+"/"+name+".class");
                int len = is.available();
                byte[] data = new byte[len];
                is.read(data);
                is.close();
                return data;
            }
    
            @Override
            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();
                }
            }
    
            @Override
            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();
    
                        //非自定义的类还是走双亲委派加载
                        if(!name.startsWith("com.cold.jvm")){
                            c = this.getParent().loadClass(name);
                        }else {
                            c = findClass(name);
                        }
    
    
                        // If still not found, then invoke findClass in order
                        // to find the class.
                        long t1 = System.nanoTime();
    
                        // 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;
                }
            }
        }
    
        public static void main(String[] args) throws Exception{
            //初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader
            MyClassLoader classLoader = new MyClassLoader("E:/test");
            //复制User.class文件到D:/test/com/cold目录下
            Class<?> clazz = classLoader.loadClass("com.cold.jvm.User");
            // 输出结果com.cold.MyClassLoaderTest$MyClassLoader
            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
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
  • 相关阅读:
    内存管理之内存寻址(笔记)
    修改中文、英文参考文献在文末列表中的顺序:EndNote
    MySQL笔记1(数据库的好处,数据库的概念,数据库的特点,MySQL的启动,数据模型,SQL)
    看完这套 Java 笔记,才明白笔者同时斩获 7 份大厂 offer 是有原因的
    python自动化测试selenium(三)下拉选择框、警告框处理、页面截图
    【EI会议征稿通知】2024年图像处理、机器学习与模式识别国际学术会议(IPMLP 2024)
    答辩步骤及相关准备1
    【藏经阁一起读】(69)__《阿里云视频云产品手册2023版》
    数据结构 —— 队列(超详细图解 & 接口函数实现)
    让Python更优雅更易读(第二集)
  • 原文地址:https://blog.csdn.net/qq_40042416/article/details/134456705