前面我们就讲过,Java 源代码文件经过编译器编译后会生成字节码文件,经过加载器加载完毕后会交给执行引擎执行。在执行的过程中,JVM 会划出来一块空间来存储程序执行期间需要用到的数据,这块空间一般被称为运行时数据区,见下图。
根据 Java 虚拟机规范的规定,运行时数据区可以分为以下几个部分:
- 程序计数器(Program Counter Register)
- Java 虚拟机栈(Java Virtual Machine Stacks)
- 本地方法栈(Native Method Stack)
- 堆(Heap)
- 方法区(Method Area)
JDK 8 开始,永久代被彻底移除,取而代之的是元空间。元空间不再是 JVM 内存的一部分,而是通过本地内存(Native Memory)来实现的。也就是说,JDK 8 开始,方法区的实现就是元空间。
程序计数器
程序计数器(Program Counter Register)所占的内存空间不大,很小很小一块,可以看作是当前线程所执行的字节码指令的行号指示器。字节码解释器会在工作的时候改变这个计数器的值来选取下一条需要执行的字节码指令,像分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。
在 JVM 中,多线程是通过线程轮流切换来获得 CPU 执行时间的,因此,在任一具体时刻,一个 CPU 的内核只会执行一条线程中的指令,因此,为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,并且不能互相干扰,否则就会影响到程序的正常执行次序。
也就是说,我们要求程序计数器是线程私有的。
《Java 虚拟机规范》中规定,如果线程执行的是非本地方法,则程序计数器中保存的是当前需要执行的指令地址;如果线程执行的是本地方法,则程序计数器中的值是 undefined。
为什么本地方法在程序计数器中的值是 undefined 的?因为本地方法大多是通过 C/C++ 实现的,并未编译成需要执行的字节码指令。
我们来通过代码以及字节码指令来看看程序计数器的作用。
public static int add(int a, int b) {
return a + b;
}
字节码指令大致如下:
0: iload_0 // 从局部变量表中加载变量 a 到操作数栈
1: iload_1 // 从局部变量表中加载变量 b 到操作数栈
2: iadd // 两数相加
3: ireturn // 返回
现在,让我们逐步分析程序计数器是如何在执行这些指令时更新的:
-
初始状态:当方法开始执行时,PC 计数器设置为 0,指向第一条指令
0: iload_0
。 -
执行第一条指令:
- 执行
iload_0
指令,将局部变量表中索引为 0 的整数(即方法的第一个参数a
)加载到操作数栈顶。 - 执行完成后,PC 计数器更新为 1,指向下一条指令
1: iload_1
。
- 执行
-
执行第二条指令:
- 执行
iload_1
指令,将局部变量表中索引为 1 的整数(即方法的第二个参数b
)加载到操作数栈顶。 - 执行完成后,PC 计数器更新为 2,指向下一条指令
2: iadd
。
- 执行
-
执行第三条指令:
- 执行
iadd
指令,弹出操作数栈顶的两个整数(即a
和b
),将它们相加,然后将结果压入操作数栈顶。 - 执行完成后,PC 计数器更新为 3,指向下一条指令
3: ireturn
。
- 执行
-
执行最后一条指令:
- 执行
ireturn
指令,弹出操作数栈顶的整数(即a + b
的结果),并将这个值作为方法的返回值。 - 方法执行完成,控制权返回到方法调用者。
- 执行
Java 虚拟机栈
Java 虚拟机栈(JVM 栈)中是一个个栈帧,每个栈帧对应一个被调用的方法。当线程执行一个方法时,会创建一个对应的栈帧,并将栈帧压入栈中。当方法执行完毕后,将栈帧从栈中移除。
栈帧包含以下 5 个部分,见下图。我们前面已经详细地讲过栈帧了,忘记的球友可以回头去看一下。
假设我们有一个简单的 add 方法,如下所示:
public int add(int a, int b) {
int result = a + b;
return result;
}
当 add
方法被调用时,JVM 为这次方法调用创建一个新的栈帧。然后执行方法内的字节码指令,这部分我们前面已经讲过了,大家可以自己通过 javap 查看字节码并模拟一下字节码指令执行的过程。
当 add
方法执行完毕后,对应的栈帧会从 JVM 栈中弹出。
Java 虚拟机栈的特点如下:
- 线程私有: 每个线程都有自己的 JVM 栈,线程之间的栈是不共享的。
- 栈溢出: 如果栈的深度超过了 JVM 栈所允许的深度,将会抛出
StackOverflowError
,这个我们讲栈帧的时候讲过了。
大家可以猜一下 JVM 栈的默认大小是多少?
还用我们之前的讲栈帧时候的例子:
public class StackOverflowErrorTest1 {
private static Atomic
回复