计算机系统应用教程网站

网站首页 > 技术文章 正文

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

btikc 2024-09-08 12:00:40 技术文章 20 ℃ 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

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表