美文网首页程序员
彻底剖析JVM类加载机制系列:初步理解类加载运行机制和类加载过程

彻底剖析JVM类加载机制系列:初步理解类加载运行机制和类加载过程

作者: 今天你敲代码了吗 | 来源:发表于2021-01-28 22:44 被阅读0次

    篇头扯皮

    文章目标:让读者初步了解类加载的过程和类加载的运行机制,明白什么是“动态链接”,什么是“静态链接”,后续会进一步更加深入

    类加载运行过程

    当我们用java命令运行某个类的main函数启动程序时,首先需要通过类加载器把主类加载到JVM中。

    以下方的Math类为例:

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

    JVM类加载过程:

    JVM类加载过程

    以windows系统为例解释:

    1. 首先,通过运行 java classload.Math.class 命令,运行字节码文件
    2. 当运行这个命令的时候,实际上,系统会使用java.exe文件(用C++语言实现),去调用jvm.dll文件中的库函数(相当于java应用里面的jar包),而这个库函数会创建Java虚拟机(C++语言实现)
    3. 在创建Java虚拟机的过程中,会创建一个引导类加载器实例(C++实现)
    4. 创建完Java虚拟机后,C++代码会去很多调用java虚拟机的启动程序,在启动的程序中会有一个sun.misc.Launcher这样子的一个类,启动Launcher类会去创建很多Java层面的类加载器(AppClassLoader等)
    5. 通过Java层面的类加载器,去加载真正的java字节码文件
    6. 把字节码文件加载完之后,c++代码会直接发起调用
    7. 程序运行结束之后,JVM进行销毁

    上面其实就是我们运行main函数后,一个具体的执行流程,在整个加载过程中,重点是弄懂,怎么把我们的Java类给加载到JVM中去的,也就是classLoader.loadClass("classLoad.Math");

    类加载过程

    所谓的类加载过程,也就是classLoader.loadClass("classLoad.Math")这一步操作,针对这一步操作,咱们先了解大体过程,具体的代码分析,之后会一步步跟下来给大家看:

    其中classLoader.loadClass("classLoad.Math")总共以下几步:

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

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

    加载

    我们知道,编译打包后的class文件是存放在磁盘中,如下图所示,那么我们首先需要做的,就是把这样的一个class文件加载到JVM内存中去,但是在丢到内存的过程中,会发生一系列的步骤,就是上述的,加载>>验证>>准备>>解析>>初始化

    编译后的class文件

    验证

    验证其实就是验证咱们字节码文件中格式的正确性,举个例子,以Math.class为例:我们看到这个文件的开头是"cafe babe",这个就说明了这个文件是一个字节码文件,如果把这个修改,JVM也就识别不了,所以说第一步验证,验证的就是字节码的内容符不符合JVM规范 在这里插入图片描述

    准备

    准备其实就把类中的静态变量做一个初始值,还是以Math类为例,我们在Math类中新建了两个静态变量,而准备这个步骤,就是把这两个静态变量做一个默认值(而不是图中的“666”或者是引用类型),int是0.,boolean是false依次类推,引用类型的话赋值成null。

    在这里插入图片描述

    温馨小提示

    图中的变量是没有加final的呦,如果加了final的话,变量就变成常量,在准备阶段就直接赋值

    解析

    先不去管什么是“符号引用”,“直接引用”这些在之后的文章中都会慢慢分析,这里先用通俗一点的话,解释个大概:

    在JVM中,方法名、类名、修饰符、返回值等等都是一系列的符号,而且这些符号都是一个个的常量,同时这些个符号、变量、代码块等等在内存中都是由一块块的内存区域来存储,这些内存区域都有对应的内存地址,而这些内存地址就是“直接引用”,而解析这个步骤就是把“符号”替换成“内存地址”

    解析这一步,在专业术语中,也叫静态链接,对应的也就有动态连接,动态连接就是在程序运行期间,完成将符号引用替换为直接引用。

    如下图所示,我们在类加载的时候,不一定把“compute”这个方法名解析成“内存地址”,只有当运行到这一行代码的时候,才会去解析这一个“符号”,因为这些符号都是一个个的常量,所以都会存放在常量池中

    我们以Math.class为例,看下动态连接到底是肿么回事:

    动态连接

    通过javap -v 命令)看看字节码文件

    public class classload.Math
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #5.#26         // java/lang/Object."<init>":()V
       #2 = Class              #27            // classload/Math
       #3 = Methodref          #2.#26         // classload/Math."<init>":()V
       #4 = Methodref          #2.#28         // classload/Math.compute:()I
       #5 = Class              #29            // java/lang/Object
       #6 = Utf8               <init>
       #7 = Utf8               ()V
       #8 = Utf8               Code
       #9 = Utf8               LineNumberTable
      #10 = Utf8               LocalVariableTable
      #11 = Utf8               this
      #12 = Utf8               Lclassload/Math;
      #13 = Utf8               compute
      #14 = Utf8               ()I
      #15 = Utf8               a
      #16 = Utf8               I
      #17 = Utf8               b
      #18 = Utf8               c
      #19 = Utf8               main
      #20 = Utf8               ([Ljava/lang/String;)V
      #21 = Utf8               args
      #22 = Utf8               [Ljava/lang/String;
      #23 = Utf8               math
      #24 = Utf8               SourceFile
      #25 = Utf8               Math.java
      #26 = NameAndType        #6:#7          // "<init>":()V
      #27 = Utf8               classload/Math
      #28 = NameAndType        #13:#14        // compute:()I
      #29 = Utf8               java/lang/Object
    {
      public classload.Math();
        descriptor: ()V
        flags: 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   Lclassload/Math;
    
      public int compute();
        descriptor: ()I
        flags: 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 7: 0
            line 8: 2
            line 9: 4
            line 10: 11
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      13     0  this   Lclassload/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: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=2, args_size=1
             0: new           #2                  // class classload/Math
             3: dup
             4: invokespecial #3                  // Method "<init>":()V
             7: astore_1
             8: aload_1
             9: invokevirtual #4                  // Method compute:()I
            12: pop
            13: return
          LineNumberTable:
            line 15: 0
            line 16: 8
            line 18: 13
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      14     0  args   [Ljava/lang/String;
                8       6     1  math   Lclassload/Math;
    }
    
    
    Constant pool就是我们的常量池,常量池中存放的就是各种各样的符号 在这里插入图片描述

    每个常量旁都有一个带“#”的,这个#1、#2就是一个标识符,在实例的创建,变量的传递,方法的调用,JVM都是用这个标识符来定位,以new Math()为例:

    在这里插入图片描述

    在main()方法中,一开始会去new一个Math()类,旁边的注释中,也指明了new的是"class classload/Math",我们接下来再来看#2指向了啥

    在这里插入图片描述

    可以看到#2是一个class,并且又去指向了一个#27,我们再跟踪到#27来看一下

    在这里插入图片描述

    可以看到#27是代表着一个类,同时编码是utf8,所以通过常量池中符号的标识符,jvm可以一步步找到创建的到底是啥玩意,方法的调用也是一样,在代码编译完之后,这些方法名、()、类名等等,都变成一个个的符号,并且存放在常量池中

    动态连接

    截止目前,编译出来的这些符号并且放到常量池,此时这个常量池是静态的,但是通过加载,放到内存后都有对应的内存地址,那么这个常量池也就会变成运行时常量池,所以动态连接需要等到运行的时候,才能把符号替换成真正的内存地址

    解析的步骤小结

    所以在类加载中,解析做的也就是“静态链接”,针对的是静态方法(例如:main方法)或者其他不变的方法,因为静态方法等到加载、分配完内存后,内存地址就不会变了,所以,可以在类加载的时候,可以直接替换成内存地址。

    但是像下图所示,由于多态的存在,像compute方法这种非静态方法,可能有不同的实现,所以在编译加载的时候是无法知道的,需要等到真正运行的时候,才能找到具体方法的实现,才能找到具体的内存地址,“动态连接”才能等到运行的时候才替换符号为内存地址

    动态连接

    初始化

    最后一步初始化,才是对类的静态变量初始化为指定的值,执行静态代码块

    在这里插入图片描述

    所以,INIT_DATA一开始是0,最后才是6666,math一开始是null,最后才是真正的内存地址。

    本文总结

    此系列是用来剖析JVM类加载机制,本文是开篇第一篇,总体先了解JVM类加载运行机制和JVM类加载机制,在JVM类加载机制中,总体分成五步,并初步介绍了各个步骤的,同时还初步了解了“静态链接”和“动态链接”,在接下来,我们会更加深入的了解JVM类加载机制,从源码的角度给大家展现JVM类加载机制,敬请期待呦

    写在最后

    大家看完有什么不懂的可以在下方留言讨论.
    谢谢你的观看。
    觉得文章对你有帮助的话记得关注我点个赞支持一下!

    作者:迷途小沙弥
    链接:https://juejin.cn/post/6922363473183637511

    相关文章

      网友评论

        本文标题:彻底剖析JVM类加载机制系列:初步理解类加载运行机制和类加载过程

        本文链接:https://www.haomeiwen.com/subject/rzhftltx.html