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函数
- 该部分上面已做分析,在此不再重复。