我们知道,java文件经过编译后转换为class文件,然后经过类加载子系统加载到jvm中执行,这个过程如下图所示:
class文件结构
编译过程就是把java文件变为class文件的过程,用javac命令就可以,比如下面一段简单的代码:
public class Math {
public static final int initData = 666;
public int cal(){
int a = 1;
int b = 2;
int c = (a+b)*10;
return c;
}
public static void main(String[] args) {
Math math = new Math();
math.cal();
}
}
经过javac编译后得到的class文件如下:
我们还可以进一步用javap反编译输出到一个txt文件
javap -p -v Math.class >Math.txt
Classfile /E:/project/jvm-case/src/main/java/com/example/jvmcase/basic/Math.class
Last modified 2020-5-31; size 442 bytes
MD5 checksum 1f3ccea6e6faf14ff0e1c70b28bf920e
Compiled from "Math.java"
public class com.example.jvmcase.basic.Math
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#20 // java/lang/Object."":()V
#2 = Class #21 // com/example/jvmcase/basic/Math
#3 = Methodref #2.#20 // com/example/jvmcase/basic/Math."":()V
#4 = Methodref #2.#22 // com/example/jvmcase/basic/Math.cal:()I
#5 = Class #23 // java/lang/Object
#6 = Utf8 initData
#7 = Utf8 I
#8 = Utf8 ConstantValue
#9 = Integer 666
#10 = Utf8
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 cal
#15 = Utf8 ()I
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V
#18 = Utf8 SourceFile
#19 = Utf8 Math.java
#20 = NameAndType #10:#11 // "":()V
#21 = Utf8 com/example/jvmcase/basic/Math
#22 = NameAndType #14:#15 // cal:()I
#23 = Utf8 java/lang/Object
{
public static final int initData;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 666
public com.example.jvmcase.basic.Math();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 3: 0
public int cal();
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 6: 0
line 7: 2
line 8: 4
line 9: 11
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 com/example/jvmcase/basic/Math
3: dup
4: invokespecial #3 // Method "":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method cal:()I
12: pop
13: return
LineNumberTable:
line 13: 0
line 14: 8
line 15: 13
}
SourceFile: "Math.java"
这个文件看起来更具有可读性,反编译后看到的内容跟class文件是一一对应的,比如说魔数,版本号信息,常量池,字段表集合,方法表集合等信息
类加载
class文件最终要运行必须要通过类加载器加载到jvm中才能正常执行,类的整个生命周期如下图,而类加载的全过程是前面三步,即装载,链接,初始化
装载
(1)通过类加载器classLoader获取一个类的全限定名来获取描述此类的二进制流,
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
(3)在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区中这些数据的访问入口
类加载器
装载的整个过程是由类加载器来完成的,当类加载器接收到类加载的请求时,它并不会马上去加载该类,而是先自低向上先检查该类是否已经加载,如果已经加载,则不会重新再加载。
如果该类没有被加载,则优先由最顶层父类加载,顶层父类没法加载的时候再给到下一个子类。
也就是说检查阶段是由低向高,加载顺序是由高向低,这就是双亲委派模型
验证
其实就是验证class文件格式的正确性,验证的内容包括魔数,版本号,常量池,方法等,其实就是对整个class文件的内容格式是否能够给当前虚拟机正常执行的一个验证。
准备
准备阶段是正式为类变量(static成员变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量
public static final int initData = 666;
这个时候initData 的值为0
解析
解析阶段虚拟机是把常量池内的符号引用替换成直接引用的过程。就是对方法、字段、类信息的一个描述落地到真正在内存中开辟一块区域。
运行时数据区
方法区跟堆是线程共享的,在虚拟机启动时创建,上面其实已经分析过了,方法区主要存的类信息,常量,静态变量,即时编译器编译后的代码等数据,堆存的是对象。
堆的内存布局如下:
整个堆分成新生代跟老年代两部分(新生代占1/3,老年代占2/3),新生代又分为eden区跟survivor区,每次创建的对象都是放在eden区,当eden区满的时候触发一次minor GC,然后把存活的对象移动到s0区,下次minor GC会把eden区跟s0区存活的对象移动到s1区,如此往复,其实就是每次minor GC的时候都会清空eden区跟survivor区的一半,然后把存活的对象移动到survivor区的另一半,并且对象每移动一次年龄就会增加1(对象头mark word有个分代年龄标记位),当年龄增加到15的时候,对象就会进入老年代。
假如这个时候创建了一个很大的对象,新生代没有足够的空间去放,那么这个对象会直接进入到老年代。
虚拟机栈
虚拟机栈是描述java方法执行的内存区域,随着线程创建而创建,每个方法在执行的时候都会创建一个栈帧,用于存贮局部变量表,操作数栈,动态链接,方法出口灯信息,比如说我们开头的例子,当执行到main方法的时候会创建一个main栈帧,main里面调用cal方法也同时创建一个cal栈帧
结合代码理解局部变量表和操作数栈
操作数栈也就是程序在运行过程中要做运算的数临时存放的一块内存区域,操作结束后数据弹出栈,同时把操作数栈清空,比如
iconst_1 将int类型常量1压入操作数栈
istore_1 将int类型值存入局部变量1
也就是将1赋给a的过程是在操作数栈中完成的,完成之后把操作数栈清空,然后把a=1放在局部变量表
public int cal(){
int a = 1;
int b = 2;
int c = (a+b)*10;
return c;
}
反编译后:
public int cal();
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 6: 0
line 7: 2
line 8: 4
line 9: 11
动态链接
比如说
Math math = new Math();
math.cal();
在math调用cal()方法的时候,通过动态链接可以找到cal()方法代码存放的位置,也就是说动态链接存放的是方法的内存位置
方法出口
当一个方法开始执行后,只有两种方式可以退出,一种是遇到方法返回的字节码指令,也就是ireturn,这个时候方法出口存放的信息就是回到main方法对应执行的cal()方法的地方,说白了就是返回上一个栈帧执行的地方;另一种是遇见异常,并且这个异常没有在方法体内得到处理。
本地方法栈
如果当前线程执行的方法是Native类型的,这些方法就会在本地方法栈中执行
程序计数器
由于java是多线程执行的,所以每个栈都会有一个私有的程序计数器,用来记录当前线程执行到的位置,当下次再抢到资源的时候就从程序计数器拿到的地址继续往下执行。
如果线程正在执行Java方法,则计数器记录的是正在执行的虚拟机字节码指令的地址;
如果正在执行的是Native方法,则这个计数器为空。