Java JVM执行流程
JDK版本是1.8.0_231,以下是官方对JVM的介绍,我简单翻译了一下,将就着看吧。
The Java Virtual Machine is the cornerstone of the Java platform. It is the component of the technology responsible for its hardware and operating system independence, the small size of its compiled code, and its ability to protect users from malicious programs .
JVM是Java平台的基石,它是计算机硬件技术层面的操作系统无关性的组件,编译后的一小段代码 ,能保护用户不熟恶意程序的攻击。
The Java Virtual Machine is an abstract computing machine. Like a real computing machine, it has an instruction set and manipulates various memory areas at run time.It is reasonably common to implement a programming language using a virtual machine; the best known virtual machine may be the P-Code machine of UCSD Pascal .
JVM是一个台抽象的计算机。像真正的计算器一样,在运行时它有一套指令集,操作各个内存区域 。使用虚拟机实现编程语言是相当普遍的,其中最知名的虚拟机可能是P-code .
这是JDK的整体结构:
给出Java程序执行的生命轨迹
接下来再详细分析一下Java 虚拟机的执行流程
1 JVM的结构
1.1class文件的格式
Java源文件被编译为可以被JVM执行的一种与操作系统无关的有一定规则的二进制数据,一般会保存在.class文件中(但不是必须的),被称为class文件的格式 。class文件能精确的定义Java类或者接口的表现形式。
现在我们新建一个简单的Java源文件JavaBean.java
public class JavaBean {
private int a = 1 ;
private static int b = 2 ;
private final int c = 3 ;
private final static int D = 4 ;
public int add(int n ,int m){
int r = n + m ;
System.out.println("n = " + n + ", m = " + m);
return r ;
}
public static void main(String[] args) {
JavaBean javaBean1 = new JavaBean();
int c = javaBean1.add(1,2) ;
System.out.println(c);
}
}
然后通过Java编译器编译为class字节码文件
执行完之后就完成了Java源文件编译为class字节码的过程,生产了JavaBean.class文件
由于class文件是二进制文件,于是我们使用16进制编码格式打开这个class字节码文件看到如下:
官方给的说明是:
A class file consists of a single ClassFile structure: ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
The items in the ClassFile structure are as follows: magic The magic item supplies the magic number identifying the class file format; it has the value 0xCAFEBABE
这个class文件可以按照这个规则详细的记录一个Java类的信息。如class文件标识,版本号,常量池,权限标识,接口,父类,方法数等等信息。
这里简单说一下
u4 magic 意思是这个文件的前4个字节是class文件的魔法标识
4字节 = 4 * 8 位 ,也就是说这个二进制文件的前32位标识的是class的标识
在16进制中,4位表示一个16进制数 ,也就是这个文件的前8位是这个magic ,值为CAFEBABE 。
到这里就很清晰了,Java是通过二进制文件的前4个字节的值是否是magic值来判断这个class文件是是合法的class文件 。
u2 minor_version; 第5到6这2个字节表示最低适配的版本号
u2 major_version; 第7到8这2个字节表示是高适配的版本号
其他的依次类推,需要详细了解的自己去查阅官方资料。
1.2 数据类型
Java中的数据分为两个大的类型,原始数据类型和引用数据类型,Java中所有的操作都是以这两种数据为基础。结构如下:
1.2.1 基础数据类型和取值
- 1 整型
数据类型 | 说明 | 取值范围 | 默认值 |
---|---|---|---|
byte | 8bit 有符号的整数 | -27 to 27-1 | 0 |
short | 16bit 有符号的整数 | -215 to 215-1 | 0 |
int | 32bit 有符号的整数 | -231 to 231-1 | 0 |
long | 64bit 有符号的整数 | -263 to 263-1 | 0 |
char | 16bit 无符号整数,表示UTF-16编码中的值 | 20 to 216-1 | ‘\u0000’ 对应的编码中的值为null |
-
2 浮点型
浮点的储存比较复杂,暂时没时间研究,待续。。。。。。。 -
3 布尔型
① boolean 值
布尔类型在早起Java版本中是不存在的,后来才加进去。虽然Java中定义了boolean类型,但是Java虚拟机没有boolean类型相关操作的指令,对布尔类型值的操作实际上在JVM中使用的int数据类型操作指令。
boolean默认值为false ,boolean的ture和false在编译器中被编译为int类型的1和0② boolean数组
但是Java虚拟机直接支持布尔数组。 它的newarray指令可以创建布尔数组。 类型数组使用字节数组指令baload和boolean访问和修改boolean 。
boolean数组其实是一个byte数组,每一个byte表示一个boolean值 -
4 returnAddress
returnAddress 数据只存在于字节码层面,与编程语言无关,也就是说,我们在 Java 语言中是不会直接与 returnAddress 类型的数据打交道的。returnAddress 类型的值就是指向特定指令内存地址的指针
官方解释:
The returnAddress type is used by the Java Virtual Machine’s jsr, ret, and jsr_w instructions . The values of the returnAddress type are pointers to the opcodes of Java Virtual Machine instructions.Unlike the numeric primitive types, the returnAddress type does not correspond to any Java programming language type and cannot be modified by the running program.
1.2.2 引用数据类型
引用类型class ,array ,inteface的值是动态创建的类的实例,数组,和实现接口的类的实例。
1.3 运行时数据区
官方解释:
The Java Virtual Machine defines various run-time data areas that are used during execution of a program. Some of these data areas are created on Java Virtual Machine start-up and are destroyed only when the Java Virtual Machine exits.Other data areas are per thread. Per-thread data areas are created when a thread iscreated and destroyed when the thread exits .
通过官方的解释可以知道,运行时数据区是Java虚拟机在运行Java程序时临时创建的各个运行时的数据区域,在Java虚拟器停止工作时运行时数据区就会销毁。
同理对应Java虚拟器中的线程(不同于计算的线程)在创建时也会创建线程私有的数据区域,当线程销毁时也会被销毁。
以下是JVM的内存模型图:
Java源文件被编译为class字节码文件之后,当JVM启动的时候,会通过类加载子系统将class加载到JVM的Run Time Data Areas(运行时数据区);
区域 | 线程私有 | 说明 |
---|---|---|
PC Register | √ | 记录线程中方法调用的当前指针,即上文中说的returnAddress,如果线程中调用的是本地方法,则记录native方法的指针 |
Native Method Stacks | √ | 如果线程中调用了非Java的本地方法,则会在线程创建的时候创建一个本地方法栈,它的功能类似Java虚拟机栈 |
JVM Stacks | √ | 线程创建的时候会创建一个线程独有的Java虚拟机栈,是一个栈的数据结构,存放着线程调用时产生的多个栈帧,执行顺序是FILO 。方法执行前是压栈,执行时,先入栈的栈帧后执行,最后入栈的栈帧先执行 |
Run Time Constant Pool | × | 运行时常量池是Method Area的一部分,用于存放编译期生成的各种字面量和符号引用,每个class都有自己的运行时常量池,这一点在class文件的格式定义中有给出。 |
Method Area | × | 方法区是Heap的一部分,存放每个class的结构如运行时常量池,字段,方法数据以及构造方法,初始化方法,特定方法[^1]信息 |
Heap | × | 用于存放所有的class的实例和数组,垃圾回收主要是针对堆内存的管理控制 |
1.4 虚拟机栈帧 Frame
官方解释:
A frame is used to store data and partial results, as well as to perform dynamic linking, return values for methods, and dispatch exceptions.
栈帧用于存储数据和部分执行的结果,以及执行动态链,返回方法执行的结果,分发方法执行中产生的异常。
A new frame is created each time a method is invoked. A frame is destroyed when its method invocation completes, whether that completion is normal or abrupt (it throws an uncaught exception).
一个新的栈帧会在一个方法被调用的时候创建,在方法执行完毕后,不管方法是否正常执行完毕还是发生异常而中断。它都会销毁。
Frames are allocated from the Java Virtual Machine stack of the thread creating the frame. Each frame has its own array of local variables ,its own operand stack , and a reference to the run-time constant pool of the class of the current method。
线程创建的栈帧都会被分配在Java虚拟机栈中,每一个栈帧都有自己的局部变量表,操作栈,和当前方法的class的运行时常量池的引用。
Frame结构图:
1.5 Java类加载过程
Java类加载过程的是将编译好的class文件加载到JVM的运行时数据的过程。
整体的加载流程图:
- 1 Loading 加载class类
加载是类加载过程中的一个阶段,这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对
象,作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个 Class 文件获取,这里既 可以从 ZIP 包中读取(比如从 jar
包和 war 包中读取),也可以在运行时计算生成(动态代理), 也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)
加载class使用的ClassLoader的结构。
说明:jdk9以后Java采用了模块化,将Extension Class Loader 改为了PlatformClassLoader 。这个以后再详细讨论。
Java的类加载机制流程如下:
当你使用一个类加载器加载class时,
1 它首先会使用它的父亲去加载,
2 然后它的父加载起又会使用自己父亲去加载,这样一直找到Bootstrap Class Loader ,如果父class loader中没有才会自己去加载
也就是说,加载class的顺序最终是先从Bootstarp ClassLoader加载,再使用Extension ClassLoader加载,再使用App ClassLoader加载,最后才会使用自己定义的类加载器加载。
在Java中sun.misc.Launcher这个类封装了Bootstrap ClassLoader , ExtClassLoader, AppClassLoader 。
这是Luancher的结构图
类的关系图:
可以看到AppClassLoader 和 ExtClassLoader都是Luancher的内部类。 而Bootstrap ClassLoader是jvm的内部实现,Java源码中只是封装了BootStrap ClassLoader加载的jar包的方法。
Class Loader | 说明 | 加载路径 |
---|---|---|
Bootstrap Class Loader | 定级类加载器,JVM内部实现 | sun.boot.class.path |
Extension Class Loader | 扩展类加载器 | java.ext.dirs |
App Class Loader | 应用程序的class加载器 | java.class.path |
User-defined Class Loader | 自定义类加载器,需要extends java.land.ClassLoader抽象类,如fastjson中的ASMClassLoader | 自定义 |
- Bootstrap ClassLoader
顶级的类加载器,是JVM内部实现的,在Java程序中无法获取实例,但是可以使用-Xbootclasspath选项或 - Dsun.boot.class.path系统属性值可以指定附加的类。也可以查看其加载的类路径,如下:
public static void main(String[] args) {
//BootStrap ClassLoader的加载路径
String bootClassPath = System.getProperty("sun.boot.class.path");
System.out.println("Bootstrap ClassLoader加载路径:"+bootClassPath);
URL[] bootstrapLoaderUrls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
System.out.println("----- Bootstrap ClassLoader------");
for (URL url : bootstrapLoaderUrls){
System.out.println(url.getPath().substring("/C:/Program%20Files/Java/".length()));
}
}
输出结果:
Bootstrap ClassLoader加载路径:C:\Program Files\Java\jdk1.8.0_231\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_231\jre\lib\rt.jar;C:\Program Files\Java\jdk1.8.0_231\jre\lib\sunrsasign.jar;C:\Program Files\Java\jdk1.8.0_231\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_231\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_231\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_231\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_231\jre\classes
----- Bootstrap ClassLoader加载的路径------
jdk1.8.0_231/jre/lib/resources.jar
jdk1.8.0_231/jre/lib/rt.jar
jdk1.8.0_231/jre/lib/sunrsasign.jar
jdk1.8.0_231/jre/lib/jsse.jar
jdk1.8.0_231/jre/lib/jce.jar
jdk1.8.0_231/jre/lib/charsets.jar
jdk1.8.0_231/jre/lib/jfr.jar
jdk1.8.0_231/jre/classes
可以看到加载的路径已经定义到了具体加载那些jar包,而实际现在的也与配置的一致。
- Extension Class Loader
顾名思义,扩展类加载器,从上图中可以看到它是Bootstrap ClassLoader的下级加载器,是AppClassLoader的父加载器。我们还是从代码来看它加载了那些类。
这是ExtClassLoader源码中获取其加载jar路径的方法
public static void main(String[] args) {
//获取系统默认的ClassLoader,默认的为AppClassLoader
ClassLoader defaultClassLoader = ClassLoader.getSystemClassLoader() ;
System.out.println("ExtClassLoader加载路径:"+System.getProperty("java.ext.dirs"));
//因此ExtClassLoader是它的上级加载器
ClassLoader extClassLoader = defaultClassLoader.getParent() ;
System.out.println("Extension 加载器: "+extClassLoader);
URL[] urls = ((URLClassLoader) extClassLoader).getURLs();
for (URL url : urls){
System.out.println(url.getPath());
}
}
输出结果:
ExtClassLoader加载路径:C:\Program Files\Java\jdk1.8.0_231\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext
Extension 加载器: sun.misc.Launcher$ExtClassLoader@63961c42
/C:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/access-bridge-64.jar
/C:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/cldrdata.jar
/C:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/dnsns.jar
/C:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/jaccess.jar
/C:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/jfxrt.jar
/C:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/localedata.jar
/C:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/nashorn.jar
/C:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/sunec.jar
/C:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/sunjce_provider.jar
/C:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/sunmscapi.jar
/C:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/sunpkcs11.jar
/C:/Program%20Files/Java/jdk1.8.0_231/jre/lib/ext/zipfs.jar
可以看到,ExtClassLoader加载的路径是:JAVA_HOME\jre\lib\ext路径下的jar包
我们再看一下这个路径下的类容:
可以发现这和打印的jar包完全一致。
- App ClassLoader
它是系统默认的类加载器,拥有加载程序classpath上所有的jar包,系统默认加载器可以通过 -Djava.system.class.loader参数去改变。下面还是通过代码来看这个加载器干了啥:
public static void main(String[] args) {
//获取系统默认的ClassLoader,默认的为AppClassLoader
ClassLoader defaultClassLoader = ClassLoader.getSystemClassLoader() ;
System.out.println("系统默认: "+defaultClassLoader);
System.out.println("AppClassLoader加载路径:"+System.getProperty("java.class.path"));
URL[] urls = ((URLClassLoader) defaultClassLoader).getURLs();
for (URL url : urls){
System.out.println(url.getPath());
}
输出结果(手动处理了之后,不处理太多,贴上来没意义):
系统默认: sun.misc.Launcher$AppClassLoader@18b4aac2
jdk1.8.0_231/jre/lib/charsets.jar
jdk1.8.0_231/jre/lib/deploy.jar
jdk1.8.0_231/jre/lib/ext/access-bridge-64.jar
jdk1.8.0_231/jre/lib/ext/cldrdata.jar
jdk1.8.0_231/jre/lib/ext/dnsns.jar
jdk1.8.0_231/jre/lib/ext/jaccess.jar
jdk1.8.0_231/jre/lib/ext/jfxrt.jar
jdk1.8.0_231/jre/lib/ext/localedata.jar
jdk1.8.0_231/jre/lib/ext/nashorn.jar
jdk1.8.0_231/jre/lib/ext/sunec.jar
jdk1.8.0_231/jre/lib/ext/sunjce_provider.jar
jdk1.8.0_231/jre/lib/ext/sunmscapi.jar
jdk1.8.0_231/jre/lib/ext/sunpkcs11.jar
jdk1.8.0_231/jre/lib/ext/zipfs.jar
jdk1.8.0_231/jre/lib/javaws.jar
jdk1.8.0_231/jre/lib/jce.jar
jdk1.8.0_231/jre/lib/jfr.jar
jdk1.8.0_231/jre/lib/jfxswt.jar
jdk1.8.0_231/jre/lib/jsse.jar
jdk1.8.0_231/jre/lib/management-agent.jar
jdk1.8.0_231/jre/lib/plugin.jar
jdk1.8.0_231/jre/lib/resources.jar
jdk1.8.0_231/jre/lib/rt.jar
myself/spring-boot-demo/target/classes/
spring-boot-starter-web/2.2.2.RELEASE/spring-boot-starter-web-2.2.2.RELEASE.jar
spring-boot-starter/2.2.2.RELEASE/spring-boot-starter-2.2.2.RELEASE.jar
spring-boot-starter-logging/2.2.2.RELEASE/spring-boot-starter-logging-2.2.2.RELEASE.jar
......
slf4j-api/1.7.29/slf4j-api-1.7.29.jar
org/springframework/spring-core/5.2.2.RELEASE/spring-core-5.2.2.RELEASE.jar
org/springframework/spring-jcl/5.2.2.RELEASE/spring-jcl-5.2.2.RELEASE.jar
JetBrains/IntelliJ%20IDEA%202019.2.3/lib/idea_rt.jar
可以看到AppClassLoader加载的路径上已经包含了ExtClassLoader和BootStrapClassLoader加载的jar。但是当获取这些class时,还是采用委派机制。首先使用BootStrapClassLoader加载核心的类,这就保证了Java中核心的class不会被用户自定义的ClassLoader加载或改变,保证了JVM的安全性。
-
2 Linking 连接,分为五个大的步骤
① Verification 验证这个阶段主要是验证class或接口的二进制流是否满足class文件结构中定义的那些标准,如果不满足会抛出 VerifyError 。
② Preparation 准备
这个阶段主要是初始化静态变量的值
③ Resolution 解析
官方说法:
Resolution is the process of dynamically determining concrete values from symbolic references in the run-time constant pool
解析是根据运行时常量池中的符号引用动态确定具体值的过程
当执行Java虚拟机指令:
anewarray,
checkcast,
getfield,
getstatic,
instanceof,
invokedynamic,
invokeinterface,
invokespecial,
invokestatic,
invokevirtual,
ldc,
ldc_w,
multianewarray,
new,
putfield,
putstatic
中任何一个创建对run-time constant pool的引用时,表示需要对symbolic reference进行解析。
Resolution是虚拟机将常量池中的符号引用替换为直接引用的过程。
符号引用( symbolic reference):
符号引用与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在 Java 虚拟机规范的 Class 文件格式中。直接引用:
直接引用可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。
解析的步骤:
④ Access Control 权限控制
Java对类,属性,方法的权限控制给了四种级别;如下:
权限 | 本身 | 同包子类 | 本身所在包 | 本身所在包之外 |
---|---|---|---|---|
pubulic | √ | √ | √ | √ |
protected | √ | √ | √ | × |
default(不写) | √ | √ | √ | × |
private | √ | × | × | × |
⑤ Overriding 重写
假如一个类的子类中的方法和其父类中的方法具有一下条件,就会覆盖父类的方法。
1 两个方法具有相同的修饰符,相同的名字,相同的参数
2 子类方法不被private修饰
3 父类的方法没有没有被private修饰
这一点在模版设计模式中使用应用的很多。
⑥ Binding Native Method Implementations 绑定本地方法实现
官方解释:
Binding is the process by which a function written in a language other than the Java programming language and implementing a native method is integrated into the Java Virtual Machine so that it can be executed. Although this process is traditionally referred to as linking, the term binding is used in the specification to avoid confusion with linking of classes or interfaces by the Java Virtual Machine.
绑定非Java实现的集成到Java虚拟机上的可以执行的本地方法,虽然这个动作有点类似与Linking操作,但是为了避免与Java中类和接口的绑定混淆,所以这里单独区分开来。
- 3 Initialization 初始化
官方说明:
Initialization of a class or interface consists of executing its classor interface initialization method .
调用类或者接口的初始化方法来初始化类或者接口。
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载
器以外,其它操作都由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码
当创建一个子类的实例时,初始化的顺序是先初始化父类再初始化子类。