一、类加载运行的全过程
1.1 类加载器初始化的过程
假如现在有一个java类 com.jvm.Math类,里面有一个main方法
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();
}
}
这个方法很简单,通常我们直接执行main方法就OK,可以运行程序了,那么点击运行main方法,整个过程是如何被加载运行的呢?为什么点击执行main方法就能得到结果呢?
首先可以看看Java命令执行代码的大体流程(宏观流程)如下:
其中loadClass的类加载器过程有如下几步:
加载 >> 验证 >> 准备 >> 解析 >> 初始化 >> 使用 >> 卸载
加载:在硬盘上查找并通过IO读入字节码文件,使用到类时才会加载,例如调用类的main()方法,new对象等等,在加载阶段会在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
验证:校验字节码文件的正确性
准备:给类的静态变量分配内存,并赋予默认值
解析:将符号引用替换为直接引用,该阶段会把一些静态方法(符号引用,比如main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成),动态链接是在程序运行期间完成的将符号引用替换为直接引用(后续会讲动态链接)
初始化:对类的静态变量初始化为指定的值,执行静态代码块
使用 :程序代码执行时使用,new出对象程序中使用。
卸载 :程序代码退出、异常、结束等,执行垃圾回收。
备注:
1.windows系统上的java启动程序是java.exe,mac系统是java。
2.c语言部分只了解,java部分才是需要掌握部分。
第一步:java调用底层的jvm.dll文件创建java虚拟机(这一步由C++实现),这里java.exe是c++写的代码,调用jvm.dll也是c++底层的一个函数,通过调用jvm.dll文件(dll文件相当于java的jar包),会创建java虚拟机,java虚拟机的启动都是c++程序实现的。
第二步:在启动虚拟机的过程中,会创建一个引导类加载器的实例,这个引导类的加载器是C语言实现的,然后jvm虚拟机就启动起来了。
第三步:接下类,c++会调用java的启动程序,刚刚只是创建了java虚拟机,java虚拟机里面还有很多启动程序,其中有一个叫做Launcher类,该类的全称是sun.misc.Launcher,通过启动这个java类,会由这个类引导加载器加载并创建很多其他的类加载器,而这些加载器才是真正启动并加载磁盘上的字节码文件。
第四步:真正去加载本地磁盘的字节码文件,然后启动执行main方法。(这一步后面会详细说,到底怎么加载本地磁盘的字节码文件)
第五步:main方法执行完毕后,引导类加载器会发起一个c++调用,销毁JVM
以上是启动一个main方法加载的全过程
下面, 我们重点来看一下, 我们的类com.jvm.Math是怎么被加载到java虚拟机里面去的?
1.2 类的加载时机
1、创建类的实例,也就是new一个对象
2、访问某个类或者接口的静态变量,或者对该静态变量赋值
3、调用类的静态方法
4、反射(Class.forName("com.jvm.Math"))
5、初始化一个类的子类(会首先初始化子类的父类)
5、JVM启动实际标明的启动类,及文件名和类名相同的那个类
1.3 类加载的过程
继续看上面的com.jvm.Math类最终会生成class字节码文件,字节码文件是怎么被加载器加载到JVM虚拟机的呢?
类加载器有五步,加载, 验证, 准备, 解析, 初始化. 那么这五步都是干什么的呢?我们来看一下
我们的类在哪里呢?在磁盘里(比如:target文件夹下的class文件),我们先要将class类加载到内存中,加载到内存区域以后,不是简简单单的转换成二进制字节码文件,他会经过一系列的过程,比如:验证、准备、解析、初始化等。把这一系列的信息转变成内元信息,放到内存里面去,我们来看看具体的过程
第一步:加载
加载阶段主要查找并加载类的二进制数据文件。在该阶段,虚拟机需要完成以下3件事情:
(1)通过一个类的全限定名来获取定义此类的二进制字节流。
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
(3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
第一步是验证字节码。
第二步:验证
验证字节码加载是否正确,比如:打开一个字节码文件。打眼一看感觉像是乱码,实际上不是的,其实这里面每一个字符都是有对应的含义,那么文件里面的内容我们能不能替换呢?当然是不能的,一旦替换,这个class文件就不能执行成功了。
验证的是什么呢?
验证字节码加载是否正确、格式是否正确、内容是否符合java虚拟机的规范。
第三步:准备
验证完了,接下来是准备操作,比如我们的类Math,他首先会给Math里面的静态变量赋值一个初始值。
准备阶段主要为类的静态变量分配内存并将其初始化为默认值,这些内存都将在方法区中分配。
该阶段有两点需要注意:
(1)首先,这时候进行内存分配的仅包括类的静态变量(static),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在java堆中。
(2)其次,这里所设置的初始值通常是数据类型默认的初始值,而不是被在Java代码中被赋予的值。这里还需要注意如下几点:
- 对基本数据类型来说,对于类的静态变量(static)和全局变量,会为其赋予默认值,而对于局部变量来说,在使用前必须显示代码中的赋值,否则编译时不通过。
- 对于同时被static和final修饰的常量,必须在声明的时候就为其显示代码中的赋值,否则编译时不通过;而只被final修饰的常量则即可以在声明时显示其赋值,也可以在类类初始化时显示赋值,总之在使用前必须赋值,系统不会为其赋予默认值。
- 对于引用数据类型来说,如数组引用,对象引用等,如果没有对其进行显示赋值而直接使用,系统都会为其赋予默认值,即为null
- 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认值。
如果类字段的字段属性表中存在ConstantValue属性(同时被final和static修饰),即在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,假设变量value被定义为:
public static final int value= 123;
public static int initData = 666;
public static User user = new User();
编译时javac就会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue设置将value赋值为123.而initData就会赋值为0
public static final int value= 123;
public static int initData = 0;
public static User user = null;
在准备过程中,就会给这个两个变量赋初始值,这个初始值并不是代码中真实的值,比如initData属性是int类型的,那么他的初始值是0,如果是boolean类型的初始值为false,对象就会赋值为null,也就是说,准备阶段的值是jvm固定的,不是我们定义的值。如果一个final的常量, 比如public static final String name="zhangsan", 是直接赋初始值"zhangsan"的. 这里只是给静态变量赋初始值
需要注意的有以下几点:
- 实例变量是在创建对象的时候完成赋值的,没有赋初值一说
- final和static修饰的常量在编译的时候会给属性添加ConstantValue属性,准备阶段直接完成赋值,即没有赋初值这一步
第四步:解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量;直接引用就是直接指向目标的指针、相对偏移量或一个间接定位的目标句柄。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符这7类符号引用进行。
注意:
1)实例变量不在该阶段分配内存
2)因为类方法和私有方法符合“编译器可知、运行期不可变”的要求,即不会被继承或重写,所以适合在类加载过程中解析
3)若类变量为常量(被final修饰),则直接赋值开发者定义的值
接下类说说解析的过程:将常量池的符号引用(间接引用)转换为直接引用,对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。
什么是符号引用呢?
比如我们的程序中的main方法,写法是固定的,我们就可以将main当成一个符号。比如上面的initData,int,static,我们都可以将其称之为符号,java虚拟机内部有个专业名词,把他叫做符号,这些符号被加载到JVM内存里都会对应一个地址,将”符号引用“转变为直接引用,指的就是,将”main、initData、int“等这些符号转变为对应的内存地址。这个地址就是代码的直接引用。根据直接引用的值,我们就可以知道代码在什么位置,然后根据地址拿到到代码去真正的运行。
将符号引用转变为“内存地址”,这种有一个专业名词,叫静态链接。上面的解析过程就是相当于静态链接的过程,在类加载期间完成符号到内存地址的转换,有静态链接,那么与之对应的就是动态链接?
什么是动态链接呢?
public static void main(String[] args) {
Math math = new Math();
math.compute();
}
比如:上面这段代码, 只有当我运行到math.compute()这句话的时候, 才会去加载compute()这个方法. 也就是说, 在加载的时候不一定会把compute()这个方法解析成内存地址. 只有当运行到这行代码的时候, 才会解析.
我们来看看汇编代码(打开该Math.class类的Termimal,然后输入下列命令)
javap -v Math.class
Classfile /E:/TuLin_case/all/target/classes/com/jvm/Math.class
Last modified 2022-6-21; size 904 bytes
MD5 checksum f8065d7c5d711691a8a358a8a5100d41
Compiled from "Math.java"
public class com.jvm.Math
minor version: 0
major version: 49
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #11.#38 // java/lang/Object."<init>":()V
#2 = Class #39 // com/jvm/Math
#3 = Methodref #2.#38 // com/jvm/Math."<init>":()V
#4 = Methodref #2.#40 // com/jvm/Math.compute:()I
#5 = Fieldref #41.#42 // java/lang/System.out:Ljava/io/PrintStream;
#6 = String #43 // ---------------
#7 = Methodref #44.#45 // java/io/PrintStream.println:(Ljava/lang/String;)V
#8 = Class #46 // com/jvm/User
#9 = Methodref #8.#38 // com/jvm/User."<init>":()V
#10 = Fieldref #2.#47 // com/jvm/Math.user:Lcom/jvm/User;
#11 = Class #48 // java/lang/Object
#12 = Utf8 initData
#13 = Utf8 I
#14 = Utf8 ConstantValue
#15 = Integer 666
#16 = Utf8 user
#17 = Utf8 Lcom/jvm/User;
#18 = Utf8 <init>
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 LocalVariableTable
#23 = Utf8 this
#24 = Utf8 Lcom/jvm/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 <clinit>
#36 = Utf8 SourceFile
#37 = Utf8 Math.java
#38 = NameAndType #18:#19 // "<init>":()V
#39 = Utf8 com/jvm/Math
#40 = NameAndType #25:#26 // compute:()I
#41 = Class #49 // java/lang/System
#42 = NameAndType #50:#51 // out:Ljava/io/PrintStream;
#43 = Utf8 ---------------
#44 = Class #52 // java/io/PrintStream
#45 = NameAndType #53:#54 // println:(Ljava/lang/String;)V
#46 = Utf8 com/jvm/User
#47 = NameAndType #16:#17 // user:Lcom/jvm/User;
#48 = Utf8 java/lang/Object
#49 = Utf8 java/lang/System
#50 = Utf8 out
#51 = Utf8 Ljava/io/PrintStream;
#52 = Utf8 java/io/PrintStream
#53 = Utf8 println
#54 = Utf8 (Ljava/lang/String;)V
{
public static final int initData;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 666
public static com.jvm.User user;
descriptor: Lcom/jvm/User;
flags: ACC_PUBLIC, ACC_STATIC
public com.jvm.Math();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_