• 彻底剖析JVM类加载机制


    本文仍然基于JDK8版本,从JDK9模块化器,类加载器有一些变动。

    0 javac编译

    java代码

    public class Math {
        public static final int initData = 666;
    
        public static User user = new User();
    
        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();
            System.out.println("end");
        }
    }
    

    javac 编译,javap -v -p 查看class文件

    Classfile /F:/workspace/advanced-java/target/classes/com/lzp/java/jvm/classloader/Math.class
    
    // 第1部分,描述信息:大小、修改时间、md5值等
      Last modified 2022年1月8日; size 1006 bytes
      MD5 checksum 4cece4543963b23a98cd219a59c1887c
      Compiled from "Math.java"
    
    // 第2部分,描述信息:编译版本
    public class com.lzp.java.jvm.classloader.Math
      minor version: 0
      major version: 52
      flags: (0x0021) ACC_PUBLIC, ACC_SUPER
      this_class: #2                          // com/lzp/java/jvm/classloader/Math
      super_class: #11                        // java/lang/Object
      interfaces: 0, fields: 2, methods: 4, attributes: 1
      
    // 第3部分,常量池信息
    Constant pool:
       #1 = Methodref          #11.#39        // java/lang/Object."<init>":()V
       #2 = Class              #40            // com/lzp/java/jvm/classloader/Math
       #3 = Methodref          #2.#39         // com/lzp/java/jvm/classloader/Math."<init>":()V
       #4 = Methodref          #2.#41         // com/lzp/java/jvm/classloader/Math.compute:()I
       #5 = Fieldref           #42.#43        // java/lang/System.out:Ljava/io/PrintStream;
       #6 = String             #44            // end
       #7 = Methodref          #45.#46        // java/io/PrintStream.println:(Ljava/lang/String;)V
       #8 = Class              #47            // com/lzp/java/jvm/classloader/User
       #9 = Methodref          #8.#39         // com/lzp/java/jvm/classloader/User."<init>":()V
      #10 = Fieldref           #2.#48         // com/lzp/java/jvm/classloader/Math.user:Lcom/lzp/java/jvm/classloader/User;
      #11 = Class              #49            // java/lang/Object
      #12 = Utf8               initData
      #13 = Utf8               I
      #14 = Utf8               ConstantValue
      #15 = Integer            666
      #16 = Utf8               user
      #17 = Utf8               Lcom/lzp/java/jvm/classloader/User;
      #18 = Utf8               <init>
      #19 = Utf8               ()V
      #20 = Utf8               Code
      #21 = Utf8               LineNumberTable
      #22 = Utf8               LocalVariableTable
      #23 = Utf8               this
      #24 = Utf8               Lcom/lzp/java/jvm/classloader/Math;
      #25 = Utf8               compute
      #26 = Utf8               ()I
      #27 = Utf8               a
      #28 = Utf8               b
      #29 = Utf8               c
      #30 = Utf8               main
      #31 = Utf8               ([Ljava/lang/String;)V
      #32 = Utf8               args
      #33 = Utf8               [Ljava/lang/String;
      #34 = Utf8               math
      #35 = Utf8               MethodParameters
      #36 = Utf8               <clinit>
      #37 = Utf8               SourceFile
      #38 = Utf8               Math.java
      #39 = NameAndType        #18:#19        // "<init>":()V
      #40 = Utf8               com/lzp/java/jvm/classloader/Math
      #41 = NameAndType        #25:#26        // compute:()I
      #42 = Class              #50            // java/lang/System
      #43 = NameAndType        #51:#52        // out:Ljava/io/PrintStream;
      #44 = Utf8               end
      #45 = Class              #53            // java/io/PrintStream
      #46 = NameAndType        #54:#55        // println:(Ljava/lang/String;)V
      #47 = Utf8               com/lzp/java/jvm/classloader/User
      #48 = NameAndType        #16:#17        // user:Lcom/lzp/java/jvm/classloader/User;
      #49 = Utf8               java/lang/Object
      #50 = Utf8               java/lang/System
      #51 = Utf8               out
      #52 = Utf8               Ljava/io/PrintStream;
      #53 = Utf8               java/io/PrintStream
      #54 = Utf8               println
      #55 = Utf8               (Ljava/lang/String;)V
    {
    // 第四部分,变量信息
      public static final int initData;
        descriptor: I
        flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
        ConstantValue: int 666
    
      public static com.lzp.java.jvm.classloader.User user;
        descriptor: Lcom/lzp/java/jvm/classloader/User;
        flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    
      public com.lzp.java.jvm.classloader.Math();
        descriptor: ()V
        flags: (0x0001) ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 3: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   Lcom/lzp/java/jvm/classloader/Math;
    // 第五部分,方法信息
      public int compute();
        descriptor: ()I
        flags: (0x0001) ACC_PUBLIC
        Code:
          stack=2, locals=4, args_size=1
             0: iconst_1
             1: istore_1
             2: iconst_2
             3: istore_2
             4: iload_1
             5: iload_2
             6: iadd
             7: bipush        10
             9: imul
            10: istore_3
            11: iload_3
            12: ireturn
          LineNumberTable:
            line 9: 0
            line 10: 2
            line 11: 4
            line 12: 11
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      13     0  this   Lcom/lzp/java/jvm/classloader/Math;
                2      11     1     a   I
                4       9     2     b   I
               11       2     3     c   I
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: (0x0009) ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=2, args_size=1
             0: new           #2                  // class com/lzp/java/jvm/classloader/Math
             3: dup
             4: invokespecial #3                  // Method "<init>":()V
             7: astore_1
             8: aload_1
             9: invokevirtual #4                  // Method compute:()I
            12: pop
            13: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
            16: ldc           #6                  // String end
            18: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
            21: return
          LineNumberTable:
            line 16: 0
            line 17: 8
            line 18: 13
            line 19: 21
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      22     0  args   [Ljava/lang/String;
                8      14     1  math   Lcom/lzp/java/jvm/classloader/Math;
        MethodParameters:
          Name                           Flags
          args
    
      static {};
        descriptor: ()V
        flags: (0x0008) ACC_STATIC
        Code:
          stack=2, locals=0, args_size=0
             0: new           #8                  // class com/lzp/java/jvm/classloader/User
             3: dup
             4: invokespecial #9                  // Method com/lzp/java/jvm/classloader/User."<init>":()V
             7: putstatic     #10                 // Field user:Lcom/lzp/java/jvm/classloader/User;
            10: return
          LineNumberTable:
            line 6: 0
    }
    

    方法中的#1/2,可以到ConstantPool找到对应符号。

    参考字节码指令表:https://docs.oracle.com/javase/specs/jvms/se8/html/index.html。

    1 类加载过程

    j经典的类加载过程如下图,包括加载、链接、初始化三部分。

    image

    1.1 加载class文件

    字节码文件位于磁盘,当使用到某个类(例如,调用main()方法,new新对象),在磁盘中查找并通过IO读取文件的二进制流,转为方法区数据结构,并存放到方法区,在Java堆中产生 java.lang.Class对象。Class对象是可以方法区的访问入口,用于Java反射机制,获取类的各种信息。

    1.2 链接过程

    验证:验证class文件是不是符合规范

    1. 文件格式的验证。验证是否以0XCAFEBABE开头,版本号是否合理

    2. 元数据验证。是否有父类,是否继承了final类(final类不能被继承),非抽象类实现了所有抽象方法。

    3. 字节码验证。(略)

    4. 符号引用验证。常量池中描述类是否存在,访问的方法或字段是否存在且有足够的权限。

    -Xverify:none  // 取消验证
    

    准备:为类的静态变量分配內存,初始化为系统的初始值

    final static修饰的变量:直接赋值为用户定义的值,比如 private final static int value=123,直接赋值123。

    private static int value=123,该阶段的值依然是0。

    解析:符号引用转换成直接引用(静态链接)

    Java代码中每个方法、方法参数都是符号,类加载放入方法区的常量池Constant pool中。

    符号引用:应该可以理解成常量池中的这些字面量。【可能没理解对】

    直接引用:符号对应代码被加载到JVM内存中的位置(指针、句柄)。

    静态链接过程在类加载时完成,主要转换一些静态方法。动态链接是在程序运行期间完成的将符号引用替换为直接引用。

    1.3 初始化(类初始化clinit-->初始化init)

    执行< clinit>方法, clinit方法由编译器自动收集类里面的所有静态变量的赋值动作及静态语句块合并而成,也叫类构造器方法

    • 初始化的顺序和源文件中的顺序一致

    • 子类的< clinit>被调用前,会先调用父类的< clinit>

    • JVM会保证clinit方法的线程安全性

    初始化时,如果实例化一个新对象,会调用<init>方法对实例变量进行初始化,并执行对应的构造方法内的代码。

    类加载过程是懒加载的,用到才会加载。

    初始化示例

    public class JVMTest2 {
        static {
            System.out.println("JVMTest2静态块");
        }
    
        {
            System.out.println("JVMTest2构造块");
        }
    
        public JVMTest2() {
            System.out.println("JVMTest2构造方法");
        }
    
        public static void main(String[] args) {
            System.out.println("main方法");
            new Sub();
        }
    }
    
    class Super {
        static {
            System.out.println("Super静态代码块");
        }
    
        public Super() {
            System.out.println("Super构造方法");
        }
    
        {
            System.out.println("Super普通代码块");
        }
    }
    
    class Sub extends Super {
        static {
            System.out.println("Sub静态代码块");
        }
    
        public Sub() {
            System.out.println("Sub构造方法");
        }
    
        {
            System.out.println("Sub普通代码块");
        }
    }
    
    JVMTest2静态块
    main方法
    Super静态代码块
    Sub静态代码块
    Super普通代码块
    Super构造方法
    Sub普通代码块
    Sub构造方法
    

    执行main方法,并不需要创建JVMTest2实例。

    对于普通代码块,以前认为是和clinit一样顺序加载。其实是不一样的,普通代码块编译时对赋值语句和其他语句分别做了优化,如下赋值语句优化为int i = 1; 打印语句优化为构造方法的第一句。

    源代码

    public class JVMTest1 {
        int i;
        {
            i = 1;
            System.out.println("JVMTest1构造块");
        }
        public JVMTest1(){
            System.out.println("JVMTest1构造方法");
        }
    }
    

    反编译后的代码

    public class JVMTest1 {
        int i = 1;
    
        public JVMTest1() {
            System.out.println("JVMTest1构造块");
            System.out.println("JVMTest1构造方法");
        }
    }
    

    2 类加载器

    查看当前JDK类加载器

    public class PrintJDKClassLoader {
        public static void main(String[] args) {
            ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
            System.out.println(systemClassLoader);
            ClassLoader parent = systemClassLoader.getParent();
            System.out.println(parent);
            ClassLoader parentParent = parent.getParent();
            System.out.println(parentParent);
        }
    }
    
    // JDK8
    sun.misc.Launcher$AppClassLoader@18b4aac2
    sun.misc.Launcher$ExtClassLoader@28a418fc
    null
    // JDK11
    jdk.internal.loader.ClassLoaders$AppClassLoader@2437c6dc
    jdk.internal.loader.ClassLoaders$PlatformClassLoader@1324409e
    null
    

    2.1 类加载器(JDK8)

    类加载器初始化过程:Java通过调用jvm.dll文件创建JVM,创建一个引导类加载器(由C++实现),通过JVM启动器(sun.misc.Launcher)加载扩展类加载器和应用类加载器。

    • 启动类加载器:负责加载lib目录下的核心类库。作为JVM的一部分,由C++实现。

    • 扩展类/平台类加载器:负责加载lib目录下的ext扩展目录中的JAR 类包。

    • 应用程序类加载器:负责加载用户类路径ClassPath路径下的类包,主要就是加载用户自己写的类。

    • 自定义类加载器:负责加载用户自定义路径下的类包。

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

    // Launcher构造方法
    public Launcher() {
        Launcher.ExtClassLoader var1;
        // 构造扩展类加载器,设置类加载器parent属性设为null。
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
        // 构造应用类加载器,设置类加载器parent属性为扩展类加载器。
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        Thread.currentThread().setContextClassLoader(this.loader);
        // 权限校验代码..
        }
    }
    

    2.2 双亲委派模型

    类加载器采用三层、双亲委派模型,类加载器的父子关系不是继承关系,而是组合关系。除了启动类加载器外,其他类加载器都是继承自ClassLoader类。

    image

    工作过程:类加载器收到类加载请求,首先判断类是否已经加载,如果未被加载,尝试将请求向上委派给父类加载器加载。当父类加载器无法完成加载任务,再由子类加载器尝试加载。

    // ClassLoader
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 检查类是否已被加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) { 
                        // 非启动类加载器
                        c = parent.loadClass(name, false);
                    } else { 
                        // 启动类加载器
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 父类加载器无法加载指定类
                }
    
                if (c == null) {
                    // 调用当前类加载器的findClass方法进行类加载
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
    

    为什么使用双亲委派模型,感觉走了弯路?

    双亲委派模型下,类加载请求总会被委派给最上层的启动类加载器。对于未加载的类来说,需要从底层走到顶层;如果用户定义的类已经被加载过,则不需要委派过程。

    使用双亲委派机制有下面几个好处:

    • 沙箱安全机制,防止核心类库代码被篡改。
    • 避免类重复加载,父类加载器加载过,子类加载器不需要再次加载。

    全盘负责委托机制

    全盘负责 :即是当一个classloader加载一个Class的时候,这个Class所依赖的和引用的其它Class 通常 也由这个classloader负责载入。 委托机制 :先让parent(父)类加载器 寻找,只有在parent找不到的时候才从自己的类路径中去寻找。

    参考Launcher构造方法

    Thread.currentThread().setContextClassLoader(this.loader);
    

    自定义类加载器

    自定义类加载器操作主要是继承ClassLoader类,重写上面源码中的findClass(name)方法。

    public class CustomClassLoaderTest {
        static class CustomClassLoader extends ClassLoader {
            private String classFilePath;
    
            public CustomClassLoader(String classFilePath) {
                this.classFilePath = classFilePath;
            }
    		// 载入class数据流
            private byte[] loadClassFile(String name) throws Exception {
                name = name.replaceAll("\\.", "/");
                FileInputStream fis = new FileInputStream(classFilePath + "/" + 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 = loadClassFile(name);
                    // 加载--链接--初始化等逻辑
                    return defineClass(name,data,0,data.length);
                } catch (Exception e) {
                    throw new ClassNotFoundException();
                }
            }
        }
    
        public static void main(String[] args) throws Exception {
            CustomClassLoader classLoader = new CustomClassLoader("F:");
            Class<?> clazz = classLoader.loadClass("com.lzp.java.jvm.classloader.JVMTest");
            Object instance = clazz.newInstance();
            Method method = clazz.getDeclaredMethod("add", null);
            System.out.println(method.invoke(instance));
            System.out.println(clazz.getClassLoader().getClass().getName());
        }
    }
    

    自定义类加载器的父加载器是应用类加载器。CustomClassLoader是使用AppClassLoader运行的,自然而然是父类加载器。

    打破双亲委派机制

    在一些场景下,打破双亲委派是必要的。例如Tomcat中可能有多个应用,引用了不同的Spring版本。打破双亲委派,可以实现应用隔离。

    JVM使用loadClass方法实现双亲委派机制。重写loadClass方法,便可以打破双亲委派机制。

    直接删除双亲委派代码是不可行的,Java代码继承自Object,总会需要双亲委派来加载核心代码。

    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                // 非自定义的类还是走双亲委派加载
                if (!name.equals("com.lzp.java.jvm.classloader.JVMTest")) {
                    c = this.getParent().loadClass(name);
                } else {
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
    

    注:JDK自带的核心库代码,是不允许自行配置修改的。例如,不可以将Object.class拷出来执行。沙箱隔离。

    image

    版权声明:本文为博主原创文章,未经博主允许不得转载。
  • 相关阅读:
    移动端布局方案汇总&&原理解析
    Javascript运行机制
    git 使用
    async await详解
    vue使用axios调用豆瓣API跨域问题
    hash和history的区别
    http状态码
    XSS 和 CSRF简述及预防措施
    【pytorch】pytorch基础学习
    [源码解读] ResNet源码解读(pytorch)
  • 原文地址:https://www.cnblogs.com/dtyy/p/15781941.html
Copyright © 2020-2023  润新知