前面我们就讲过,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的结果),并将这个值作为方法的返回值。 - 方法执行完成...
- 执行
回复