Skip to content

程序计数器:线程私有的执行引擎

你知道 CPU 是怎么知道下一条该执行什么指令的吗?

在物理 CPU 中,程序计数器(PC)是一个寄存器,存放着下一条指令的地址。JVM 中的程序计数器,本质上是一样的——只不过 JVM 运行的是字节码,而不是机器码。


一、它的作用是什么?

程序计数器(Program Counter Register),也称为 PC 寄存器,是一小块内存空间。

每个线程都有自己的程序计数器,互不干扰,这与 CPU 的设计完全一致。

它的作用很明确:

  1. 存储当前指令地址:如果当前执行的是 Java 方法,存储的是正在执行的字节码指令的地址
  2. 支持分支、循环、跳转:字节码执行 gotoif_icmp 等指令时,需要修改 PC 的值
  3. 恢复执行位置:线程切换时,保存当前执行位置;切换回来时,从保存的位置继续执行
java
public class PCRegisterDemo {

    public int method() {
        int a = 1;
        int b = 2;
        int c = a + b;
        if (c > 10) {
            return c;
        }
        return 0;
    }
}

当 JVM 执行这个方法时,程序计数器会依次记录每条字节码指令的位置。当 if_c > 10 为 true 时,PC 会跳转到 return c 对应的指令位置。


二、Native 方法的特殊情况

如果执行的是 Native 方法(通过 JNI 调用本地代码),程序计数器的值是 undefined(未定义)。

这是因为 Native 方法通常调用的是 C/C++ 代码,这些代码由操作系统直接执行,不归 JVM 管理。

Native 方法调用
┌─────────────────────────────────────┐
│  Java 代码执行区                      │
│  PC 寄存器 → 字节码地址               │
└─────────────────────────────────────┘
              ↓ JNI 调用
┌─────────────────────────────────────┐
│  Native 代码执行区                    │
│  PC 寄存器 → undefined               │
│  (由操作系统管理)                    │
└─────────────────────────────────────┘

三、唯一一个没有 OOM 的区域

在 JVM 的所有内存区域中,程序计数器是唯一一个不会抛出 OutOfMemoryError 的区域

为什么?

因为它的空间是固定的——对于一个线程来说,程序计数器只需要存储一个指针(返回地址或字节码地址)。无论你怎么折腾 JVM,都不可能让程序计数器耗尽内存。

区域可能出现的异常原因
程序计数器空间固定,不存在 OOM
虚拟机栈StackOverflowError / OOM栈深度或线程数超限
OutOfMemoryError对象过多
方法区OutOfMemoryError类过多

四、线程切换与执行位置保存

理解了程序计数器的作用,你就能理解线程切换是怎么工作的:

时间线:
T1: 线程A执行 → PC = 10(记录位置)
        ↓ 线程切换(时间片用完)
T2: 线程B执行 → PC = 25(记录位置)
        ↓ 线程切换(时间片用完)
T3: 线程A恢复 → 读取 PC = 10,继续执行

当操作系统决定切换线程时,JVM 会:

  1. 保存线程 A 的程序计数器(PC = 10)
  2. 恢复线程 B 的程序计数器(PC = 25)
  3. 切换到线程 B 执行

这就是为什么程序计数器必须是线程私有的——每个线程的执行进度不同,必须独立记录。


五、面试常考点

问题 1:什么情况下 PC 寄存器的值是 undefined?

执行 Native 方法时。Native 方法由 C/C++ 实现,执行权交给操作系统,JVM 不再记录字节码位置。

问题 2:程序计数器会GC吗?

不会。程序计数器是线程私有的,随线程创建而创建,随线程消亡而释放(或者被回收)。它不参与垃圾回收。

问题 3:为什么需要程序计数器?

多线程环境下,CPU 在不同线程间切换。当线程重新获得 CPU 时间片时,需要知道从哪里继续执行——程序计数器就是用来记录这个位置的。


留给你的问题

程序计数器存储的是字节码指令地址,而不是指令本身。

你有没有想过:为什么 JVM 选择存储地址,而不是把整个指令都存下来?

实际上,这是冯·诺依曼架构的基本思想——代码(指令)和数据是分开存储的,程序计数器本质上是一个"指针",指向代码区。

但这也引出了一个问题:如果两个线程同时执行同一个方法,它们的程序计数器会一样吗?

答案是:会的,但各自独立。就像同一个教室里有两个人同时看同一本书,各自在自己的便签上标记"看到第几页"——书是同一本书,但标记是各自的。

下一节,我们来聊聊虚拟机栈——这里才是存储方法调用细节的地方。

基于 VitePress 构建