彻底剖析JVM类加载机制系列:初步理解类加载运行机制和类加载过程
篇头扯皮
文章目标:让读者初步了解类加载的过程和类加载的运行机制,明白什么是“动态链接”,什么是“静态链接”,后续会进一步更加深入
类加载运行过程
当我们用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系统为例解释:
- 首先,通过运行 java classload.Math.class 命令,运行字节码文件
- 当运行这个命令的时候,实际上,系统会使用java.exe文件(用C++语言实现),去调用jvm.dll文件中的库函数(相当于java应用里面的jar包),而这个库函数会创建Java虚拟机(C++语言实现)
- 在创建Java虚拟机的过程中,会创建一个引导类加载器实例(C++实现)
- 创建完Java虚拟机后,C++代码会去很多调用java虚拟机的启动程序,在启动的程序中会有一个sun.misc.Launcher这样子的一个类,启动Launcher类会去创建很多Java层面的类加载器(AppClassLoader等)
- 通过Java层面的类加载器,去加载真正的java字节码文件
- 把字节码文件加载完之后,c++代码会直接发起调用
- 程序运行结束之后,JVM进行销毁
上面其实就是我们运行main函数后,一个具体的执行流程,在整个加载过程中,重点是弄懂,怎么把我们的Java类给加载到JVM中去的,也就是classLoader.loadClass("classLoad.Math");
类加载过程
所谓的类加载过程,也就是classLoader.loadClass("classLoad.Math")这一步操作,针对这一步操作,咱们先了解大体过程,具体的代码分析,之后会一步步跟下来给大家看:
其中classLoader.loadClass("classLoad.Math")总共以下几步:
加载>>验证>>准备>>解析>>初始化>>使用>>卸载
- 加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载(懒加载),例如:调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
- 验证:校验字节码文件的正确性
- 准备:给类的静态变量分配内存,并赋予默认值
- 解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main方法,替换为指向数据所存内存的指针或句柄等(直接引用)),这就是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间,完成将符号引用替换为直接引用。
- 初始化:对类的静态变量初始化为指定的值,执行静态代码块
加载
我们知道,编译打包后的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