Skip to content

03.1 - 程序计数器

定位: 每个线程当前执行的字节码行号指示器——JVM 中最小的内存区域 面试高频度: ⭐⭐ 考查方式: 为什么不需要 OOM?Native 方法时的值?线程上下文切换中的作用?

一、这是什么?为什么需要它?

程序计数器(Program Counter Register)是当前线程所执行的字节码行号指示器。每个线程独立拥有一个,互不干扰。

为什么需要它? 关键在于 CPU 时间片轮转(Time-Slicing):

时间线 ──────────────────────────────────────────────────────→

线程A  ████████████████████░░░░░░░░░░████████████████████
线程B  ░░░░░░░░░░░░░░░░░░░░████████████░░░░░░░░░░░░░░░░░░
线程C  ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░████████████████

         线程A运行中    线程B获得CPU    线程A重新获得CPU
                       (PC保存A的位置)  (从PC恢复A的位置)

当 CPU 从线程 A 切换到线程 B 时,线程 A 的执行位置必须被保存。等线程 A 再次获得 CPU 时间片时,它需要精确知道"我上次执行到哪条字节码了"——这就是 PC 的工作。

没有 PC,线程被切换后无法恢复执行。这是上下文切换的基础设施

二、原理拆解

2.1 存储内容

PC 中存储的是下一条要执行的字节码指令地址(行号)。执行引擎通过 PC 读取字节码,然后自动更新 PC:

PC → 指向当前方法在方法区中的字节码偏移量

执行引擎: 读取 PC 所指的字节码指令

执行指令 (如: iconst_1, istore_0)

PC 自动 +1 (或 +N, 取决于指令长度)

重复...

2.2 Native 方法的 PC 值

当线程执行的是 Native 方法(JNI 调用进入 C/C++ 代码)时,PC 的值是 undefined

原因很简单:Native 方法执行的是机器码指令,不是 JVM 字节码。机器码的"行号"由 CPU 自身的指令指针寄存器(RIP/EIP)记录,JVM 的 PC 寄存器对此一无所知。

Java 方法执行时:                          Native 方法执行时:
┌─────────────────────────────┐           ┌─────────────────────────────┐
│ PC = 0x0034 (字节码偏移量)    │           │ PC = undefined             │
│                             │           │                             │
│ 字节码指令:                   │           │ 机器码指令:                  │
│  0x0034: aload_0            │           │  CPU 的 RIP 寄存器记录位置    │
│  0x0035: getfield #2        │           │  JVM 不跟踪 Native 执行位置   │
│  0x0038: ireturn            │           └─────────────────────────────┘
└─────────────────────────────┘

2.3 为什么是唯一不会 OOM 的区域?

JVM 规范明确规定:程序计数器区域不存在 OOM(OutOfMemoryError)条件

区域空间来源是否动态增长可能异常
程序计数器固定大小(一个机器字长)从不增长无(规范定义)
虚拟机栈每个线程分配固定/动态栈可动态扩展StackOverflowError / OOM
JVM 启动时分配 + 动态扩展持续增长OOM
方法区元空间可动态扩展持续增长OOM
  • PC 的大小是编译器确定的——一个 int 或指针宽度(32/64 位),运行时从不动态扩展
  • PC 的生命周期与线程完全绑定:线程创建 → PC 分配,线程结束 → PC 释放
  • 不存在"PC 内存泄漏"的场景——没有动态分配,没有残留引用

整个 JVM 规范中,只有程序计数器是唯一被明确声明"不会发生 OOM"的区域。

三、图解全景

┌─────────────────────────────────────────────────────────────┐
│                  CPU 时间片调度与 PC                          │
│                                                             │
│  CPU Core                                                   │
│  ┌──────────────────────┐                                   │
│  │  执行引擎              │                                   │
│  │  ┌─ 读取 PC → 执行 ─┐│                                   │
│  │  │   → 更新 PC       ││                                   │
│  │  └──────────────────┘│                                   │
│  └──────────┬───────────┘                                   │
│             │ 上下文切换                                       │
│             ▼                                                 │
│  ┌──────────────────────────────────────────────────────┐   │
│  │  线程表 (Thread Table)                                │   │
│  │  ┌─────────┬─────────────┬─────────────────────────┐ │   │
│  │  │ 线程    │  PC 值      │  状态                    │ │   │
│  │  ├─────────┼─────────────┼─────────────────────────┤ │   │
│  │  │ T1      │ 0x0042     │ RUNNING (当前执行)        │ │   │
│  │  │ T2      │ 0x008A     │ WAITING (上次执行到此)    │ │   │
│  │  │ T3      │ 0x00C3     │ BLOCKED (上次执行到此)    │ │   │
│  │  └─────────┴─────────────┴─────────────────────────┘ │   │
│  └──────────────────────────────────────────────────────┘   │
│                                                             │
│  当 CPU 切换回 T2:                                          │
│  → 从线程表读取 T2 的 PC = 0x008A                           │
│  → 执行引擎从方法区中偏移 0x008A 的字节码开始执行             │
│  → 就好像从未被中断过                                          │
└─────────────────────────────────────────────────────────────┘

四、实战验证

PC 寄存器对 Java 代码不可见——没有 API 能直接读取它的值。但我们可以通过线程 Dump 间接验证 PC 的存在:

bash
# 打印 Java 进程的线程栈 (包含每个线程的执行位置)
jstack <pid>

# 或者使用 jcmd
jcmd <pid> Thread.print

线程 Dump 输出的每一帧都相当于 PC 的高级表示——它告诉你"这个线程正在执行哪个方法的哪一行"。

bash
"thread-1" #11 prio=5 os_prio=0 cpu=12.34ms elapsed=5.67s
  java.lang.Thread.sleep(Native Method)          ← Native: PC undefined
  at com.example.Worker.run(Worker.java:42)        Java: PC 指向 .java:42
  at java.lang.Thread.run(Thread.java:748)         Java: PC 指向 .java:748

五、面试视角

追问答案要点
程序计数器会 OOM 吗?不会。JVM 规范明确规定不存在 OOM 条件。大小固定(一个机器字长),无动态扩展,生命周期绑定线程,不存在内存泄漏路径
Native 方法执行时,PC 的值是什么?undefined。Native 方法执行机器码而非字节码,由 CPU 的 RIP/EIP 寄存器管理执行位置,JVM 不跟踪
程序计数器的作用是什么?记录当前线程下一条要执行的字节码指令地址。CPU 时间片轮转时,线程被切换回来通过 PC 恢复执行位置
每个线程有几个 PC?一个。JVM 启动时就确定每个线程独立拥有一个 PC 寄存器,互不干扰

📚 相关链接

  • **Java虚拟机栈** — 栈帧中也有"返回地址"概念,与 PC 配合实现方法调用和返回
  • **本地方法栈** — Native 方法切换时,PC 被标记为 undefined
  • ← 返回 **运行时数据区索引**

Knowledge4J — Java 知识库