【深入理解JVM 一】一个Java程序的执行流程
本篇是《深入理解JVM》系列博客的第一篇,旨在全局把控,先对整体流程有个认识,然后再分阶段详解。程序从编写到执行整体可以划分为以下几个步骤:编辑源码->编译生成class文件->(加载class文件、运行class字节码文件、垃圾回收),其中后两个步骤都是在jvm虚拟机上执行的,整体的执行流程如下:
编辑生成源代码
编辑源代码是经历的第一个环节,编辑源代码,就是我们在任何一个工具上编写源代码,可以是记事本,也可以是IDE,这部分相当于我们在IDEA上 新建一个.java的Class 然后写内容,这里我们创建几个类和接口:
//父类Person
public class Person {
//成员变量
private String name;
private int age;
//构造方法
public Person(int age, String name){
this.age = age;
this.name = name;
}
//成员方法
public void run(){
}
}
//接口IStudy
public interface IStudy {
int study(int a, int b);
}
真正的Strudent类,实现接口IStudy 和继承父类Person:
public class Student extends Person implements IStudy {
//私有静态成员变量
private static int cnt=5;
//静态方法块
static{
cnt++;
}
//私有成员变量
private String sid;
//构造方法
public Student(int age, String name, String sid){
//继承父类构造方法
super(age,name);
this.sid = sid;
}
//父类方法重写
public void run(){
System.out.println("run()...");
}
//实现接口方法
public int study(int a, int b){
int c = 10;
int d = 20;
return a+b*c-d;
}
//成员方法
public static int getCnt(){
return cnt;
}
//方法加载入口
public static void main(String[] args){
Student s = new Student(28,"tml","20210201");
//接口方法调用
s.study(5,6);
//成员方法调用
Student.getCnt();
//父类重写方法调用
s.run();
}
}
可以从文件路径看到已经有三个Java文件生成了:
编译生成字节码
编译的过程就是生成.class
字节码文件,输入命令javac Student.java
将该源码文件.java
编译生成.class
字节码文件。由于在源码文件中定义了两个类,一个接口,所以生成了3个.clsss文件。
字节码文件是真正实现Java语言跨平台的基石,JVM运行的是class字节码文件,只要是这种格式的文件就行
- 各种不同平台的虚拟机都统一使用这种相同的程序存储格式 跨平台
- 其他语言编写的源码编译成字节码文件,交给jvm去运行,只要是合法的字节码文件,JVM都会正确地跑起来。 跨语言
所以其实可以看的出它实现了跨平台和跨语言,用一张图可以描述清楚
在类文件夹目录下可以执行类问题,用命令javap -c Student
执行类class文件结构如下 :
"C:\Program Files\Java\jdk1.8.0_251\bin\javap.exe" -c Student.class
Compiled from "Student.java"
public class com.company.Student extends com.company.Person implements com.company.IStudy {
public com.company.Student(int, java.lang.String, java.lang.String);
Code:
0: aload_0
1: iload_1
2: aload_2
3: invokespecial #1 // Method com/company/Person."<init>":(ILjava/lang/String;)V
6: aload_0
7: aload_3
8: putfield #2 // Field sid:Ljava/lang/String;
11: return
public void run();
Code:
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String run()...
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
public int study(int, int);
Code:
0: bipush 10
2: istore_3
3: bipush 20
5: istore 4
7: iload_1
8: iload_2
9: iload_3
10: imul
11: iadd
12: iload 4
14: isub
15: ireturn
public static int getCnt();
Code:
0: getstatic #6 // Field cnt:I
3: ireturn
public static void main(java.lang.String[]);
Code:
0: new #7 // class com/company/Student
3: dup
4: bipush 28
6: ldc #8 // String tml
8: ldc #9 // String 20210201
10: invokespecial #10 // Method "<init>":(ILjava/lang/String;Ljava/lang/String;)V
13: astore_1
14: aload_1
15: iconst_5
16: bipush 6
18: invokevirtual #11 // Method study:(II)I
21: pop
22: invokestatic #12 // Method getCnt:()I
25: pop
26: aload_1
27: invokevirtual #13 // Method run:()V
30: return
static {};
Code:
0: iconst_5
1: putstatic #6 // Field cnt:I
4: getstatic #6 // Field cnt:I
7: iconst_1
8: iadd
9: putstatic #6 // Field cnt:I
12: return
}
Process finished with exit code 0
可以看到字节码文件存放了这个类的各种信息:字段、方法、父类、实现的接口等各种信息
IDEA配置字节码查看方式
在IDEA的Settings里进行相关配置即可,配置截图如下:
填写的相关属性信息如下:
- 其中Name为工具的名称,可以随意填写,这里我命名:
ShowByteCode
- Program表示你所使用的程序,这里我们选择jdk里面的javap工具。点击文件夹找到路径即可,我的路径为:
C:\Program Files\Java\jdk1.8.0_251\bin\javap.exe
- Arguments表示你使用Program程序时跟随的参数,这里直接填写
-c $FileNameWithoutExtension$.class
- Working directory表示当前工作目录,这里直接填写:
$OutputPath$\$FileDirRelativeToSourcepath$
配置好以上信息在对应的类右键查看字节码即可:
IDEA集成Jclasslib
File -> setting -> plugins路径下直接安装jclasslib:
重启后直接打开view 查看:
可以直观的看到每个方法调用时局部变量表的内容以及操作数栈的操作,如果用Jclasslib来查看字节码比IDEA集成的看起来更清晰一些,内容如下,
// class version 52.0 (52)
// access flags 0x21
public class com/company/Student extends com/company/Person implements com/company/IStudy {
// compiled from: Student.java
// access flags 0xA
private static I cnt
// access flags 0x2
private Ljava/lang/String; sid
// access flags 0x1
public <init>(ILjava/lang/String;Ljava/lang/String;)V
L0
LINENUMBER 15 L0
ALOAD 0
ILOAD 1
ALOAD 2
INVOKESPECIAL com/company/Person.<init> (ILjava/lang/String;)V
L1
LINENUMBER 16 L1
ALOAD 0
ALOAD 3
PUTFIELD com/company/Student.sid : Ljava/lang/String;
L2
LINENUMBER 17 L2
RETURN
L3
LOCALVARIABLE this Lcom/company/Student; L0 L3 0
LOCALVARIABLE age I L0 L3 1
LOCALVARIABLE name Ljava/lang/String; L0 L3 2
LOCALVARIABLE sid Ljava/lang/String; L0 L3 3
MAXSTACK = 3
MAXLOCALS = 4
// access flags 0x1
public run()V
L0
LINENUMBER 21 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "run()..."
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L1
LINENUMBER 23 L1
RETURN
L2
LOCALVARIABLE this Lcom/company/Student; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
// access flags 0x1
public study(II)I
L0
LINENUMBER 26 L0
BIPUSH 10
ISTORE 3
L1
LINENUMBER 27 L1
BIPUSH 20
ISTORE 4
L2
LINENUMBER 28 L2
ILOAD 1
ILOAD 2
ILOAD 3
IMUL
IADD
ILOAD 4
ISUB
IRETURN
L3
LOCALVARIABLE this Lcom/company/Student; L0 L3 0
LOCALVARIABLE a I L0 L3 1
LOCALVARIABLE b I L0 L3 2
LOCALVARIABLE c I L1 L3 3
LOCALVARIABLE d I L2 L3 4
MAXSTACK = 3
MAXLOCALS = 5
// access flags 0x9
public static getCnt()I
L0
LINENUMBER 32 L0
GETSTATIC com/company/Student.cnt : I
IRETURN
MAXSTACK = 1
MAXLOCALS = 0
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 38 L0
NEW com/company/Student
DUP
BIPUSH 28
LDC "tml"
LDC "20210201"
INVOKESPECIAL com/company/Student.<init> (ILjava/lang/String;Ljava/lang/String;)V
ASTORE 1
L1
LINENUMBER 40 L1
ALOAD 1
ICONST_5
BIPUSH 6
INVOKEVIRTUAL com/company/Student.study (II)I
POP
L2
LINENUMBER 42 L2
INVOKESTATIC com/company/Student.getCnt ()I
POP
L3
LINENUMBER 44 L3
ALOAD 1
INVOKEVIRTUAL com/company/Student.run ()V
L4
LINENUMBER 46 L4
RETURN
L5
LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
LOCALVARIABLE s Lcom/company/Student; L1 L5 1
MAXSTACK = 5
MAXLOCALS = 2
// access flags 0x8
static <clinit>()V
L0
LINENUMBER 5 L0
ICONST_5
PUTSTATIC com/company/Student.cnt : I
L1
LINENUMBER 8 L1
GETSTATIC com/company/Student.cnt : I
ICONST_1
IADD
PUTSTATIC com/company/Student.cnt : I
L2
LINENUMBER 9 L2
RETURN
MAXSTACK = 2
MAXLOCALS = 0
}
JVM加载执行字节码文件
在命令行中输入java Student
这个命令,就启动了一个java虚拟机,然后加载Student.class字节码文件到内存,然后运行内存中的字节码指令了。这部分的操作就相当于我们在IDEA这样的ide上 点击运行按钮。整个字节码class文件执行的整体宏观如图所示:
虚拟机JVM负责核心的加载.class文件、将.class文件转为机器码,最终执行机器码。JVM的功能模块主要包括类加载器、执行引擎和垃圾回收系统。同时JVM有自己的内存模型。
JVM内存模型
JVM中把内存分为方法区、Java栈、Java堆、本地方法栈、PC寄存器 5部分数据区域:
- 方法区:用于存放类、接口、常量以及静态变量等元数据信息,加载进来的字节码数据都存储在方法区
- 虚拟机栈 :执行引擎运行字节码时的运行时内存区,采用栈帧的形式保存每个方法的调用运行数据
- 本地方法栈:执行引擎调用本地方法时的运行时内存区
- Java堆:运行时数据区,各种对象一般都存储在堆上
- PC寄存器(程序计数器):功能如同CPU中的PC寄存器,指示要执行的字节码指令。
这部分内容在 Jvm运行时内存分析这篇blog详细解析
类加载过程
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化五个阶段,其中验证、准备、解析三个部分统称链接
各个阶段需要对代码操作内容如下:
- 加载阶段
1)类加载器会在指定的classpath中找到Student.class(通过类的全限定名)这个文件,然后读取字节流中的数据,将其存储在方法区中。
2)类加载器根据Student.class的信息建立一个Class对象,这个对象比较特殊,一般也存放在方法区中,用于作为运行时访问Student类的各种数据的接口。 - 验证阶段:必要的验证工作,格式、语义等
- 准备阶段: 为Student中的静态字段分配内存空间,也是在方法区中,并进行零初始化,即数字类型初始化为0,boolean初始化为false,引用类型初始化为null等。
private static int cnt=5;
此时,并不会执行赋值为5的操作,而是将其初始化为0。 - 解析阶段:由于已经加载到内存了,所以原来字节码文件中存放的部分方法、字段等的符号引用可以解析为其在内存中的直接引用了,而不一定非要等到真正运行时才进行解析。
- 初始化阶段:初始化阶段是执行类构造器()方法的过程,是真正意义上开始执行阶段
类加载过程中主要是将class文件(准确地讲,应该是类的二进制字节流)加载到虚拟机内存中,真正执行字节码的操作,在加载完成后才真正开始
运行字节码指令
执行引擎找到main这个入口方法,执行其中的字节码指令,这里就依赖到了Java栈,也就是虚拟机栈,其结构如下:
每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法称为当前方法(Current Method)。执行引 擎运行的所有字节码指令都只针对当前栈帧进行操作
- 当前正在运行的方法的栈帧位于栈顶,当前方法返回,则当前方法对应的栈帧出栈,当前方法的调用者的栈帧变为栈顶
- 当前方法的方法体中若是调用了其他方法,则为被调用的方法创建栈帧,并将其压入栈顶。简单查看我们上述方法的运行过程:
注意当存在方法调用关系时遵循以上两个原则
main方法执行
main方法对应的下面是我们执行的代码:
//方法加载入口
public static void main(String[] args){
Student s = new Student(28,"tml","20210201");
//接口方法调用
s.study(5,6);
//成员方法调用
Student.getCnt();
//父类重写方法调用
s.run();
}
其对应的字节码指令如下:
public static void main(java.lang.String[]);
Code:
0: new #7 // class com/company/Student
3: dup
4: bipush 28
6: ldc #8 // String tml
8: ldc #9 // String 20210201
10: invokespecial #10 // Method "<init>":(ILjava/lang/String;Ljava/lang/String;)V
13: astore_1
14: aload_1
15: iconst_5
16: bipush 6
18: invokevirtual #11 // Method study:(II)I
21: pop
22: invokestatic #12 // Method getCnt:()I
25: pop
26: aload_1
27: invokevirtual #13 // Method run:()V
30: return
首先会在虚拟机栈中为main方法创建栈帧,局部变量表长度为2,slot0存放参数args,slot1存放局部变量Student s,操作数栈最大深度为5:
然后执行new#7
指令,在java堆中创建一个Student对象,并将其引用值放入main栈帧中
最后初始化一个对象所需的参数入操作数栈(通过实例构造的方式):
up
:复制栈顶的值,然后将复制的结果入操作数栈。bipush 28
:将单字节常量值28入操作数栈。ldc #8
:将#8这个常量池中的常量即tml
取出,并入操作数栈。ldc #9
:将#9这个常量池中的常量即20210201
取出,并入操作数栈。
入栈后的整体效果如下
Strudent构造方法执行
invokespecial #10
:调用#10这个常量所代表的方法,即Student.<init>()
这个方法,这步是为了初始化对象s的各项值,<init>()
方法,是编译器将调用父类的<init>()
的语句、构造代码块、实例字段赋值语句,以及自己编写的构造方法中的语句整合在一起生成的一个方法。保证调用父类的<init>()
方法在最开头,自己编写的构造方法语句在最后,而构造代码块及实例字段赋值语句按出现的顺序按序整合到<init>()
方法中。此时需注意:上边从dup到ldc #9这四条指令向栈中添加了4个数据,而Student.()方法刚好也需要4个参数:
//构造方法
public Student(int age, String name, String sid){
//继承父类构造方法
super(age,name);
this.sid = sid;
}
其对应的字节码文件为:
public com.company.Student(int, java.lang.String, java.lang.String);
Code:
0: aload_0
1: iload_1
2: aload_2
3: invokespecial #1 // Method com/company/Person."<init>":(ILjava/lang/String;)V
6: aload_0
7: aload_3
8: putfield #2 // Field sid:Ljava/lang/String;
11: return
虽然定义中只显式地定义了传入3个参数,而实际上会隐含传入一个当前对象的引用作为第一个参数,所以四个参数依次为this,age,name,sid。上面的4条指令刚好把这四个参数的值依次入栈,进行参数传递,然后调用了Student.<init>()
方法,会创建该方法的栈帧,并入栈。栈帧中的局部变量表的第0到4个slot分别保存着入栈的那四个参数值。创建Studet.<init>()
方法的栈帧:
Student.<init>()
方法中的字节码指令如下:
-
aload_0
:将局部变量表slot0处的引用值入操作数栈 -
aload_1
:将局部变量表slot1处的int值入操作数栈 -
aload_2
:将局部变量表slot2处的引用值入操作数栈
-
invokespecial #1
:调用Person.<init>()
方法,同调用Student.<init>
过程类似,创建栈帧,将三个参数的值存放到局部变量表等,并且给堆上的对象赋值
-
从
Person.<init>()
返回之后,用于传参的操作数栈的3个值被回收了, Person栈帧的使命完成。 -
aload_0
:将slot0处的引用值入栈,也就是父类构造好的引用。 -
aload_3
:将slot3处的引用值入栈,也就是子类独有的参数sid。 -
putfield #2
:将当前栈顶的值”20210201”赋值给0x2222所引用对象的sid字段,然后两个参数出栈。 -
return
:返回调用方即main()方法,当前方法栈帧出栈,main栈帧上操作数栈上使用的几个slot销毁,只保留了最底部的引用0x222
回到main方法中
重新回到main()方法中,继续执行下面的字节码指令:
public static void main(java.lang.String[]);
Code:
0: new #7 // class com/company/Student
3: dup
4: bipush 28
6: ldc #8 // String tml
8: ldc #9 // String 20210201
10: invokespecial #10 // Method "<init>":(ILjava/lang/String;Ljava/lang/String;)V
13: astore_1
14: aload_1
15: iconst_5
16: bipush 6
18: invokevirtual #11 // Method study:(II)I
21: pop
22: invokestatic #12 // Method getCnt:()I
25: pop
26: aload_1
27: invokevirtual #13 // Method run:()V
30: return
astore_1
:将当前栈顶引用类型的值赋值给slot1处的局部变量,然后出栈。
接下来继续执行main方法的相关指令:
aload_1
:slot1处的引用类型的值入栈,也就是要开始使用s对象的方法,所以先将引用入栈iconst_5
:将常数5入栈,int型常数只有0-5有对应的iconst_x指令bipush 6
:将变量6入栈invokevirtual #11
:调用虚方法study(),这个方法是重写的接口中的方法,需要动态分派,所以使用了invokevirtual指令。
Study方法的执行
构造方法执行完成后,即顺序执行study方法的调用,代码如下:
//实现接口方法
public int study(int a, int b){
int c = 10;
int d = 20;
return a+b*c-d;
}
字节码如下:
public int study(int, int);
Code:
0: bipush 10
2: istore_3
3: bipush 20
5: istore 4
7: iload_1
8: iload_2
9: iload_3
10: imul
11: iadd
12: iload 4
14: isub
15: ireturn
最大栈深度3,局部变量表5
字节码指令执行如下:
- bipush 10:将10入栈
- istore_3:将栈顶的10赋值给slot3处的int局部变量,即c,出栈。
- bipush 20:将20入栈
- istore 4:将栈顶的20赋值给slot4处的int局部变量,即d,出栈。上面4条指令,完成对c和d的赋值工作。
iload_1、iload_2、iload_3
这三条指令将slot1、slot2、slot3这三个局部变量入栈:
imul
:将栈顶的两个值出栈,相乘的结果入栈,也就是计算b*c:
iadd
:将当前栈顶的两个值出栈,相加的结果入栈,也就是计算a+b*ciload 4
:将slot4处的int型的局部变量入栈,也就是将d入栈isub
:将栈顶两个值出栈,相减结果入栈,也就是计算a+b*c-d:
ireturn
:将当前栈顶的值返回到调用方,到这里为止study方法执行完毕,study栈帧出虚拟机栈,main栈帧上的操作数栈清空,方法继续向下执行。
以上study的方法就执行完成了。
回到main方法中
重新回到main()方法中,继续执行下面的字节码指令:
public static void main(java.lang.String[]);
Code:
0: new #7 // class com/company/Student
3: dup
4: bipush 28
6: ldc #8 // String tml
8: ldc #9 // String 20210201
10: invokespecial #10 // Method "<init>":(ILjava/lang/String;Ljava/lang/String;)V
13: astore_1
14: aload_1
15: iconst_5
16: bipush 6
18: invokevirtual #11 // Method study:(II)I
21: pop
22: invokestatic #12 // Method getCnt:()I
25: pop
26: aload_1
27: invokevirtual #13 // Method run:()V
30: return
下面的字节码指令就不再重复画图示意了:
invokestatic #12
调用静态方法getCnt()不需要传任何参数pop
:getCnt()方法有返回值,将其出栈aload_1
:将slot1处的引用值入栈invokevirtual #13
:调用0x2222对象的run()
方法,重写自父类的方法,需要动态分派,所以使用invokevirtual指令return
:main()返回,程序运行结束。
这样整体的main方法也执行完毕,整个程序调用完毕。
总结
总结而言,从一个Java程序被编写,最后一直到创建的对象被垃圾回收,全流程包括以下几步,加粗部分为本系列接下来的blog重点讲解内容:
- 编辑生成源代码.java文件
- 编译(javac编译和jit编译)生成字节码文件
- 类文件被加载到虚拟机(类Class文件结构,虚拟机运行时内存分析,类加载机制)
- 虚拟机执行二进制字节码(虚拟机字节码执行系统)
- 垃圾回收(JVM垃圾回收机制)
虚拟机发挥作用的部分从第3步到第5步之间:
- 类加载阶段:一个类文件首先加载到方法区,一些符号引用被解析(静态解析)为直接引用或者等到运行时分派(动态绑定),经过一系列的加载过程(class文件的常量池被加载到方法区的运行时常量池,各种其它的静态存储结构被加载为方法区运行时数据解构等等),程序通过Class对象来访问方法区里的各种类型数据
- 字节码执行阶段:当加载完之后,程序发现了main方法,也就是程序入口,那么程序就在栈里创建了一个栈帧,逐行读取方法里的代码所转换为的指令,而这些指令大多已经被解析为直接引用了,那么程序通过持有这些直接引用使用指令去方法区中寻找变量对应的字面量来进行方法操作。
- JVM垃圾回收阶段:操作完成后方法返回给调用方,该栈帧出栈。内存空间被GC回收,堆里被new的那些也就被垃圾回收机制GC了。
以上就是整个Java程序的生命周期。
ghcnbsg: 不对劲啊,这比我论文还要详尽的文章怎么就没人看呢
java架构师uuid(): 过滤器在初始化时候init方法只会在生命周期中执行一次,但是其中的doFilter方法会在每次请求中都会执行,并不是在你说的一个实例只能被调用一次,如果只能被调用一次,那岂不是后来的请求都无法被拦截,如何达到验证、日志、字符串过滤的功能?
小王毕业啦: 博主的文章真的让我眼前一亮,对于数据库选型这个话题,我以前总是感觉一知半解,但是在博主的详细解读下,我终于对这个领域有了更深入的了解。博主的细节描写非常到位,让我感受到了博主在这个领域的深厚功底,对于系统架构设计也有了更清晰的认识。期待博主未来能够持续分享更多干货文章,让我们读者都能够从中受益。同时,也希望能够得到博主的指导,一同进步。非常感谢博主的辛苦分享和支持!
qq_33766275: 有一个叫trid的软件,靠检索文件开头是否是“PK”,同时内部是否有“DOCUMENT.XML.RELS”“CONTENT_TYPES”“_RELS”“WORD”等字符串来区分zip和docx
JAVA菜鸟程序员: 大佬,堆栈信息是从哪里点进去的?