【JVM原理】运行时数据区(内存结构)
JVM (Java Virtual Machine)原理
文章目录
- 四、运行时数据区(内存结构)
- 4-1 线程私有区域
- 程序计数器(program counter Register)
- 本地方法栈(Native Method Stacks)
- Java 虚拟机栈(Java Virtual Machine Stacks)
- 局部变量表(Local Variable Table)
- 操作数栈(Operand Stack)
- 动态链接(Dynamic Linking)
- 方法返回地址(Return Address)
- 4-2 线程共享区域
- 方法区(Method Area)
- 运行时常量池(Run-Time Constant Pool)
- 堆(Heap)
前情提要:
如果你还没看过【JVM原理】类加载机制 请跳转阅读
四、运行时数据区(内存结构)
oracle官方文档:
- Chapter 2. The Structure of the Java Virtual Machine (oracle.com)
Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。
4-1 线程私有区域
程序计数器(program counter Register)
程序计数器是一个较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。每个线程都有一个独立的程序计数器,用于记录线程当前执行的位置。
本地方法栈(Native Method Stacks)
本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别只是虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。一些虚拟机(如 HotSpot)直接把本地方法栈和虚拟机栈合二为一。
Java 虚拟机栈(Java Virtual Machine Stacks)
它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储 局部变量表(Local Variable Table)、操作数栈(Operand Stack)、动态链接(Dynamic Linking)、方法返回地址(Return Address)等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
- 动态链接:在程序运行阶段,由符号引用转化为直接引用
- 静态链接:在解析阶段,由符号引用转为直接引用
假设我们有一个简单的 Java 方法,该方法接收两个整数参数,计算它们的和,并返回结果:
public class Example {
public static void main(String[] args) {
int sum = add(10, 20);
System.out.println("The sum is " + sum);
}
public static int add(int a, int b) {
int result = a + b;
return result;
}
}
编译 Java 源代码并查看生成的字节码的命令
javac Example.java
javap -c Example.class > Example.txt
Compiled from "Example.java"
public class Example {
public Example();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: bipush 10
2: bipush 20
4: invokestatic #2 // Method add:(II)I
7: istore_1
8: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
11: new #4 // class java/lang/StringBuilder
14: dup
15: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
18: ldc #6 // String The sum is
20: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: iload_1
24: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
27: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
30: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
33: return
public static int add(int, int);
Code:
0: iload_0
1: iload_1
2: iadd
3: istore_2
4: iload_2
5: ireturn
}
逐行解释
Example
类的构造方法:
aload_0加载this引用到操作数栈中。
invokespecial #1调用Object类的构造函数<init>,初始化this对象。
return返回,构造方法结束。
main
方法的字节码:
// 加载整数
0: bipush 10 将整数10压入操作数栈。
2: bipush 20 将整数20压入操作数栈。
// 调用静态方法
3: invokestatic #2 调用静态方法add,该方法接收两个整数参数并返回一个整数。此时,操作数栈中有两个整数(20和10),它们作为参数传递给add方法。
// 存储结果
7: istore_1 将add方法的返回值(即两个整数相加的结果)存储到局部变量表的第二个槽(索引1)中。
8: getstatic #3 加载System.out字段的引用到操作数栈。
// 创建 StringBuilder 对象
11: new #4 创建一个新的StringBuilder对象,并将引用压入栈顶。
14: dup 复制栈顶的StringBuilder对象引用,使得栈中有两个引用。
15: invokespecial #5 调用StringBuilder的构造函数初始化对象。
// 附加字符串
18: ldc #6 将字符串"The sum is "加载到栈顶。
20: invokevirtual #7 调用StringBuilder对象的append方法,将字符串附加到StringBuilder对象上。
// 附加整数
23: iload_1 从局部变量表的第二个槽(索引1)中加载add方法的返回值。
24: invokevirtual #8 调用StringBuilder.append(int)方法,将整数附加到StringBuilder对象上。
// 转换为字符串并打印
27: invokevirtual #9 调用StringBuilder.toString()方法,将StringBuilder对象转换为字符串,并将字符串压入栈顶。
30: invokevirtual #10 调用PrintStream对象的println方法,打印栈顶的字符串。
33: return 结束方法。
StringBuilder
对象的操作:"The sum is "
是从常量池加载到栈顶,然后与从局部变量表加载的整数结果拼接在一起,形成最终的输出字符串。
其中的数字表示程序计数器的指向,如
2: bipush20
表示执行完bipush 10
后,程序计数器更新为2
add
方法的字节码:
0: iload_0 将局部变量表中索引0位置的第一个整数参数加载到栈顶。
1: iload_1 将局部变量表中索引1位置的第二个整数参数加载到栈顶。
2: iadd 取栈顶的两个整数相加,并将结果压回栈顶。
3: istore_2 将栈顶的整数结果存储到局部变量表的索引2位置。
4: iload_2 再次将局部变量表中索引2位置的整数加载到栈顶。
5: ireturn 返回栈顶的整数值。
输出详细的类文件信息
javap -v Example.class > dynamiclink.txt
Classfile /D:/Example.class
Last modified 2024-9-13; size 706 bytes
MD5 checksum fd7f75a854358ac89e66464a14018d24
Compiled from "Example.java"
public class Example
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #12.#23 // java/lang/Object."<init>":()V
#2 = Methodref #11.#24 // Example.add:(II)I
#3 = Fieldref #25.#26 // java/lang/System.out:Ljava/io/PrintStream;
#4 = Class #27 // java/lang/StringBuilder
#5 = Methodref #4.#23 // java/lang/StringBuilder."<init>":()V
#6 = String #28 // The sum is
#7 = Methodref #4.#29 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#8 = Methodref #4.#30 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#9 = Methodref #4.#31 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#10 = Methodref #32.#33 // java/io/PrintStream.println:(Ljava/lang/String;)V
#11 = Class #34 // Example
#12 = Class #35 // java/lang/Object
#13 = Utf8 <init>
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 main
#18 = Utf8 ([Ljava/lang/String;)V
#19 = Utf8 add
#20 = Utf8 (II)I
#21 = Utf8 SourceFile
#22 = Utf8 Example.java
#23 = NameAndType #13:#14 // "<init>":()V
#24 = NameAndType #19:#20 // add:(II)I
#25 = Class #36 // java/lang/System
#26 = NameAndType #37:#38 // out:Ljava/io/PrintStream;
#27 = Utf8 java/lang/StringBuilder
#28 = Utf8 The sum is
#29 = NameAndType #39:#40 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#30 = NameAndType #39:#41 // append:(I)Ljava/lang/StringBuilder;
#31 = NameAndType #42:#43 // toString:()Ljava/lang/String;
#32 = Class #44 // java/io/PrintStream
#33 = NameAndType #45:#46 // println:(Ljava/lang/String;)V
#34 = Utf8 Example
#35 = Utf8 java/lang/Object
#36 = Utf8 java/lang/System
#37 = Utf8 out
#38 = Utf8 Ljava/io/PrintStream;
#39 = Utf8 append
#40 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#41 = Utf8 (I)Ljava/lang/StringBuilder;
#42 = Utf8 toString
#43 = Utf8 ()Ljava/lang/String;
#44 = Utf8 java/io/PrintStream
#45 = Utf8 println
#46 = Utf8 (Ljava/lang/String;)V
{
public Example();
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=2, args_size=1
0: bipush 10
2: bipush 20
4: invokestatic #2 // Method add:(II)I
7: istore_1
8: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
11: new #4 // class java/lang/StringBuilder
14: dup
15: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V
18: ldc #6 // String The sum is
20: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: iload_1
24: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
27: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
30: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
33: return
LineNumberTable:
line 3: 0
line 4: 8
line 5: 33
public static int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=2
0: iload_0
1: iload_1
2: iadd
3: istore_2
4: iload_2
5: ireturn
LineNumberTable:
line 7: 0
line 8: 4
}
SourceFile: "Example.java"
javap -v Example.class
输出的内容包括了
① 类文件的基本信息,如:文件路径、最后修改日期、文件大小、MD5校验和及编译源文件。
Classfile /D:/Example.class
Last modified 2024-9-13; size 706 bytes
MD5 checksum fd7f75a854358ac89e66464a14018d24
Compiled from "Example.java"
② 类定义部分描述了类的基本属性如:minor version
次版本号、major version
主版本号等。
public class Example
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
③ Constant pool 常量池用于存储类定义中用到的各种常量,包括符号引用(如类和接口的全限定名、字段名和方法名)、常量值(如字符串和数字)等。每一行代表一个常量池条目,编号从#1
开始。例如,#1
是一个方法引用,指向java/lang/Object
类的"<init>"
方法,其描述符为()V
,表示这是一个无参数无返回值的方法。
④ 接下来的部分展示了类中的方法定义。包括:构造方法、main
方法和 add
方法。
在理解了字节码代表含义的基础上,下面逐一解释在这个例子中的局部变量表、操作数栈、动态链接以及方法返回地址的工作原理。
局部变量表(Local Variable Table)
局部变量表是用来存储方法内部使用的局部变量的数据结构。每个方法都有自己的局部变量表,它是一组变量槽(slots)的集合,每个槽可以存储一个Java虚拟机基本类型的值(如int, float, reference等)或者一个对象引用。
构造方法中
locals=1 表示局部变量表有1个槽。
args_size=1 表示构造方法有一个参数(this引用)。
0: aload_0 加载this引用,0是局部变量表中的第一个槽,这里存放的是this引用。
main
方法中
locals=2 表示局部变量表有两个槽。
args_size=1 表示main方法有一个参数(String[] args)该参数存储于局部变量表的第1个槽中
0: bipush 10 和 2: bipush 20 不直接涉及局部变量表,它们是将整数直接压入操作数栈。
7: istore_1 将操作数栈顶部的整数结果存储到局部变量表的第2个槽中。
操作数栈(Operand Stack)
操作数栈是一个用于暂存数据的后进先出(LIFO)栈。在方法执行过程中,操作数栈用于临时存放中间计算结果和操作数。
main
方法
0: bipush 10 将整数10压入操作数栈。
2: bipush 20 将整数20压入操作数栈。
4: invokestatic #2 调用静态方法 add,从栈中弹出两个整数参数,并将返回的结果(整数和)压入栈中。
7: istore_1 将栈顶的整数结果存储到局部变量表中。
动态链接(Dynamic Linking)
动态链接是指在类文件被加载到JVM时,JVM 解析常量池中的符号引用为直接引用的过程。例如,当执行invokestatic #2
指令时,JVM 需要知道 add
方法的具体位置。这需要 JVM 根据 #2
的符号引用 Example.add:(II)I
解析出实际的方法引用,并准备好调用该方法。
方法返回地址(Return Address)
方法退出指的是方法执行完毕,控制权返回给调用者的过程。在字节码中,通常通过 return
指令来表示方法的结束。
- 在构造方法中,
4: return
指令表示构造方法执行完成。 - 在
main
方法中,33: return
指令表示main
方法执行完成。 - 在
add
方法中,5: ireturn
指令表示方法执行完成,并返回一个整数值。
总结来说,局部变量表用于存储方法中的局部变量,操作数栈用于临时存储计算过程中的数据,动态链接用于解析方法调用时的符号引用,而方法返回地址则是通过特定的返回指令来表示方法的结束。
4-2 线程共享区域
方法区(Method Area)
方法区(也称为非堆区)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 “Non-Heap” (非堆),目的是与 Java 堆区分开来。方法区不需要连续的内存空间,也有关于它是否固定大小或者可扩展的选择,甚至可以选择不实现垃圾回收。(jdk1.8 以前 hotspot虚拟机叫永久代、持久代, jdk1.8 时叫元空间)
运行时常量池(Run-Time Constant Pool)
Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。对于常量池中的各种字符串字面量和符号引用,虚拟机提供了特定的方法给予创建和访问。
堆(Heap)
堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
堆是垃圾收集器管理的主要区域,因此也被称作GC堆(Garbage Collected Heap)。从内存回收的角度来看,堆可以细分为年轻代(Young Generation)和老年代(Old Generation)。