1. 先来看看JVM运行时数据区的结构
- 线程独占: 每个线程都有它独立的空间,随线程生命周期而创建和销毁。
- 线程共享: 所有线程能访问这块内存数据,随虚拟机GC 而创建和销毁。
方法区
- JVM 用来存储加载的类信息、常量、静态变量、编译后的代码等数据。
- 虚拟机规范中,这是一个逻辑区域。
- 具体实现根据不同虚拟机来实现。
- 如 oracle 的 HotSpot 在 java7 中方法区放在永久代,java8 放在元数据空间,并且通过 GC 机制对这个区域进行管理。
堆内存
- 堆内存可以分为:
- 老年代
- 新生代
- Eden
- From Survivor
- To Survivor
- JVM 启动时创建,存放对象的实例。
- 垃圾回收期主要就是管理堆内存。如果满了,就会出现
OutOfMemoryError
。
虚拟机栈
- 每个线程在这个空间有一个私有的空间。
- 线程栈由多个栈帧(Stack Frame)组成。
- 一个线程会执行一个或多个方法,一个方法对应一个栈帧。
- 栈帧内容包含: 局部变量表、操作数栈、动态链接、方法返回地址、附加信息等。
- 栈内存默认最大是 1M,超出则抛出
StackOverflowError
。
本地方法栈
- 和虚拟机栈功能类似,虚拟机栈是为虚拟机执行 JAVA 方法而准备的,本地方法栈是为虚拟机使用 Native 本地方法而准备的。
- 虚拟机规范没有规定具体的实现,由不同的虚拟机厂商去实现。
- HotSpot 虚拟机中虚拟机栈和本地方法栈的实现是一样的。同样,超出大小以后也会抛出
StackOverflowError
。
程序计数器(Program Counter Register)
- 记录当前线程执行字节码的位置,存储的是字节码指令地址,如果执行 Native 方法,则计数器值为空。
- 每个线程都在这个空间有一个私有的空间,占用内存空间很少。
- CPU 同一时间,只会执行一条线程中的指令。JVM 多线程会轮流切换并分配 CPU 执行时间的方式。为了线程切换后,需要通过程序计数器,来恢复正确的执行位置。
2. 接下来看看我们经常提到的字节码文件吧
1. 先搞一个测试代码
public class Demo1 {
public static void main(String[] args) {
int x = 500;
int y = 100;
int a = x / y;
int b = 50;
System.out.println(a + b);
}
}
2. 编译并生成class文件
# 编译
javac Demo1.java
# 查看文件内容
javap -v Demo1.class > Demo.txt
3. 接下来看看Demo.txt文件都有些什么吧
针对 class 文件的官方描述(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.1)
Classfile /Users/shadowolf/Demo1.class
Last modified 2019-11-7; size 414 bytes
MD5 checksum ae6fa820973681b35609c75631cb255b
Compiled from "Demo1.java"
public class Demo1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #17.#18 // java/io/PrintStream.println:(I)V
#4 = Class #19 // Demo1
#5 = Class #20 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 main
#11 = Utf8 ([Ljava/lang/String;)V
#12 = Utf8 SourceFile
#13 = Utf8 Demo1.java
#14 = NameAndType #6:#7 // "<init>":()V
#15 = Class #21 // java/lang/System
#16 = NameAndType #22:#23 // out:Ljava/io/PrintStream;
#17 = Class #24 // java/io/PrintStream
#18 = NameAndType #25:#26 // println:(I)V
#19 = Utf8 Demo1
#20 = Utf8 java/lang/Object
#21 = Utf8 java/lang/System
#22 = Utf8 out
#23 = Utf8 Ljava/io/PrintStream;
#24 = Utf8 java/io/PrintStream
#25 = Utf8 println
#26 = Utf8 (I)V
{
public Demo1();
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 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=5, args_size=1
0: sipush 500
3: istore_1
4: bipush 100
6: istore_2
7: iload_1
8: iload_2
9: idiv
10: istore_3
11: bipush 50
13: istore 4
15: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
18: iload_3
19: iload 4
21: iadd
22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
25: return
LineNumberTable:
line 3: 0
line 4: 4
line 5: 7
line 6: 11
line 7: 15
line 8: 25
}
SourceFile: "Demo1.java"
Classfile
Classfile /Users/shadowolf/Demo1.class
Last modified 2019-11-7; size 414 bytes
MD5 checksum ae6fa820973681b35609c75631cb255b
Compiled from "Demo1.java"
- 主要记录了一些文件的信息,包括文件本地地址、文件大小、最后更新时间、MD5校验、编译来源等。
public class Demo1
public class Demo1
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
- 这一块主要描述编译的一些信息。
- major version: 主版本号,minor version: 次版本号,以下是版本的对应关系。
JDK版本 | major.minor version |
1.1 | 45 |
1.2 | 46 |
1.3 | 47 |
1.4 | 48 |
1.5 | 49 |
1.6 | 50 |
1.7 | 51 |
1.8 | 52 |
- 剩下的自己往下计算便可。
- flags: 访问标志。如下是访问标志列表及解释
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x0001 | 是否为 public 类型 |
ACC_FINAL | 0x0010 | 是否被声明为 final,只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用 invokespecial 字节码指令,JDK12 之后编译出来的类的这个标示为 true |
ACC_INTERFACE | 0x0200 | 标志这个是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型,对于接口或抽象类来说,此标志值为 true,其他值为 false |
ACC_SYNTHETIC | 0x1000 | 标志这个类并非️用户产生的 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
Constant pool
Constant pool:
#1 = Methodref #5.#14 // java/lang/Object."<init>":()V
#2 = Fieldref #15.#16 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #17.#18 // java/io/PrintStream.println:(I)V
#4 = Class #19 // Demo1
#5 = Class #20 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 main
#11 = Utf8 ([Ljava/lang/String;)V
#12 = Utf8 SourceFile
#13 = Utf8 Demo1.java
#14 = NameAndType #6:#7 // "<init>":()V
#15 = Class #21 // java/lang/System
#16 = NameAndType #22:#23 // out:Ljava/io/PrintStream;
#17 = Class #24 // java/io/PrintStream
#18 = NameAndType #25:#26 // println:(I)V
#19 = Utf8 Demo1
#20 = Utf8 java/lang/Object
#21 = Utf8 java/lang/System
#22 = Utf8 out
#23 = Utf8 Ljava/io/PrintStream;
#24 = Utf8 java/io/PrintStream
#25 = Utf8 println
#26 = Utf8 (I)V
- 常量池。
- 关于常量池的详细理解,推荐查看博客(http://softlab.sdut.edu.cn/blog/subaochen/2018/12/java-class%E6%96%87%E4%BB%B6%E7%BB%93%E6%9E%84%EF%BC%9A%E5%B8%B8%E9%87%8F%E6%B1%A0/)
- 列举一下常量表项
类型 | 描述 |
CONSTANT_utf8_info | UTF-8 编码的字符串 |
CONSTANT_Integer_info | 整型字面量 |
CONSTANT_Float_info | 浮点型字面量 |
CONSTANT_Long_info | 长整型字面量 |
CONSTANT_Double_info | 双精度浮点型字面量 |
CONSTANT_Class_info | 类或接口的符号引用 |
CONSTANT_String_info | 字符串类型字面量 |
CONSTANT_Fieldref_info | 字段的符号引用 |
CONSTANT_Methodref_info | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 字段或方法的符号引用 |
CONSTANT_MethodType_info | 标志方法类型 |
CONSTANT_MethodHandle_info | 表示方法句柄 |
CONSTANT_InvokeDynamic_info | 表示一个动态方法调用点 |
构造方法
public Demo1();
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 1: 0
- Demo1 中,我们并没有写构造函数。
- 由此可见,没有定义构造函数时,会有隐式的无参构造函数。
- descriptor: ()V -> 对于这个东西的理解,是入参为空,返回值为 void
入口函数: main 函数
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=5, args_size=1
0: sipush 500
3: istore_1
4: bipush 100
6: istore_2
7: iload_1
8: iload_2
9: idiv
10: istore_3
11: bipush 50
13: istore 4
15: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
18: iload_3
19: iload 4
21: iadd
22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
25: return
LineNumberTable:
line 3: 0
line 4: 4
line 5: 7
line 6: 11
line 7: 15
line 8: 25
- 我们来看看整个程序的执行顺序
0: sipush 500
: 将500压入操作数栈
序号 | 本地变量表 |
0 | args |
操作数栈 |
500 |
3: istore_1
: 将500保存到本地变量表1的位置
序号 | 本地变量表 |
0 | args |
1 | 500 |
4: bipush 100
: 将100压入操作数栈
序号 | 本地变量表 |
0 | args |
1 | 500 |
操作数栈 |
100 |
6: istore_2
: 将100保存到本地变量表2的位置
序号 | 本地变量表 |
0 | args |
1 | 500 |
2 | 100 |
7: iload_1
、8: iload_2
: 将本地变量表1、2位置的数据压入操作数栈
序号 | 本地变量表 |
0 | args |
1 | 500 |
2 | 100 |
操作数栈 |
100 |
500 |
9: idiv
: 进行除法运算,并且将结果压入操作数栈
序号 | 本地变量表 |
0 | args |
1 | 500 |
2 | 100 |
操作数栈 |
5 |
10: istore_3
: 将5(500/100)保存到本地变量表3的位置
序号 | 本地变量表 |
0 | args |
1 | 500 |
2 | 100 |
3 | 5 |
11: bipush 50
: 将50压入操作数栈
序号 | 本地变量表 |
0 | args |
1 | 500 |
2 | 100 |
3 | 5 |
操作数栈 |
50 |
13: istore 4
: 将50保存到本地变量表4的位置
序号 | 本地变量表 |
0 | args |
1 | 500 |
2 | 100 |
3 | 5 |
4 | 50 |
15: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
: 将常量池中#2
对应的常量压入操作数栈
序号 | 本地变量表 |
0 | args |
1 | 500 |
2 | 100 |
3 | 5 |
4 | 50 |
操作数栈 |
#2 |
18: iload_3
: 将本地变量表中3位置的数据(5)压入操作数栈
序号 | 本地变量表 |
0 | args |
1 | 500 |
2 | 100 |
3 | 5 |
4 | 50 |
操作数栈 |
5 |
#2 |
19: iload 4
: 将本地变量表中4位置的数据(50)压入操作数栈
序号 | 本地变量表 |
0 | args |
1 | 500 |
2 | 100 |
3 | 5 |
4 | 50 |
操作数栈 |
50 |
5 |
#2 |
21: iadd
: 将栈的前两个元素执行加法操作,并将执行结果(50+5=55)压入操作数栈
序号 | 本地变量表 |
0 | args |
1 | 500 |
2 | 100 |
3 | 5 |
4 | 50 |
操作数栈 |
55 |
#2 |
22: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
: jvm回根据这个方法的描述,创建新栈帧,方法的参数从操作数栈中弹出,压入虚拟机栈中,然后虚拟机栈会开始执行虚拟机栈最上面的栈帧。25: return
: 执行完毕,返回来继续执行main方法,返回,main方法结束。- 至此,我们的整个main函数的执行过程便解释完了。
3. 看看整体函数的运行分析吧
1. 加载信息到方法区
2. JVM创建线程来执行
3. 执行main函数
- 该部分上面已做分析,在此不再重复。