程序员scholar 程序员scholar
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
首页
  • Java 基础

    • JavaSE
    • JavaIO
    • JavaAPI速查
  • Java 高级

    • JUC
    • JVM
    • Java新特性
    • 设计模式
  • Web 开发

    • Servlet
    • Java网络编程
  • Web 标准

    • HTML
    • CSS
    • JavaScript
  • 前端框架

    • Vue2
    • Vue3
    • Vue3 + TS
    • 微信小程序
    • uni-app
  • 工具与库

    • jQuery
    • Ajax
    • Axios
    • Webpack
    • Vuex
    • WebSocket
    • 第三方登录
  • 后端与语言扩展

    • ES6
    • Typescript
    • node.js
  • Element-UI
  • Apache ECharts
  • 数据结构
  • HTTP协议
  • HTTPS协议
  • 计算机网络
  • Linux常用命令
  • Windows常用命令
  • SQL数据库

    • MySQL
    • MySQL速查
  • NoSQL数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • RabbitMQ
  • 服务器

    • Nginx
  • Spring框架

    • Spring6
    • SpringMVC
    • SpringBoot
    • SpringSecurity
  • SpringCould微服务

    • SpringCloud基础
    • 微服务之DDD架构思想
  • 日常必备

    • 开发常用工具包
    • Hutoll工具包
    • IDEA常用配置
    • 开发笔记
    • 日常记录
    • 项目部署
    • 网站导航
    • 产品学习
    • 英语学习
  • 代码管理

    • Maven
    • Git教程
    • Git小乌龟教程
  • 运维工具

    • Docker
    • Jenkins
    • Kubernetes
  • 算法笔记

    • 算法思想
    • 刷题笔记
  • 面试问题常见

    • 十大经典排序算法
    • 面试常见问题集锦
关于
GitHub (opens new window)
npm

(进入注册为作者充电)

  • Java底层 - JVM

    • JVM - Java体系结构
    • JVM - 类加载子系统
    • JVM - 运行时数据区概述及线程
    • JVM - 程序计数器
      • 1. 程序计数器简介
      • 2. 程序计数器的核心作用
      • 3. 代码与字节码示例
      • 4. PC 寄存器在线程切换中的关键作用
      • 5. 常见面试题解析
        • 5.1 为什么 PC 寄存器必须是线程私有的?
        • 5.2 什么是 CPU 时间片?它与 PC 寄存器有什么关系?
    • JVM - 虚拟机栈
    • JVM - 本地方法接口
    • JVM - 本地方法栈
    • JVM - 堆 (Heap)
    • JVM - 方法区
    • JVM - 对象实例化内存布局
    • JVM - 直接内存管理
    • JVM - 执行引擎
    • JVM - 字符串常量池 (StringTable)
    • JVM - 垃圾回收概述
    • JVM - 垃圾回收相关算法
    • JVM - 垃圾回收相关概念
    • JVM - 垃圾回收器
    • JVM - Class文件结构
    • JVM - 字节码指令集与解析
    • JVM - 类的加载过程详解
    • JVM - 再谈类的加载器
    • JVM - 调优概述
    • JVM - 监控及诊断工具cmd
    • JVM - 监控及诊断工具GUI
    • JVM - 运行时参数
    • JVM - 分析GC日志
  • Java底层
  • Java底层 - JVM
scholar
2024-01-17
目录

JVM - 程序计数器

# 1. 程序计数器简介

参考官方文档

《Java虚拟机规范(Java SE 8版)》第 2.5.1 节: 《Java虚拟机规范(Java SE 8版)》第 2.5.1 节 (opens new window)

程序计数器(Program Counter Register,简称 PC 寄存器) 是 JVM 运行时数据区中一个非常重要但又相对简单的部分。它的概念源自于物理 CPU 中的程序计数器,其核心功能是存储当前线程下一条将要执行的指令地址。

在物理 CPU 中,寄存器是高速存储部件,用于暂存指令、数据和地址。CPU 执行指令时,依赖程序计数器来确定下一条指令的内存地址。JVM 中的 PC 寄存器是对这种物理概念的一种抽象模拟,它并非直接操作物理寄存器,而是在 JVM 内部实现的一个逻辑概念。因此,更贴切地称之为“PC 计数器”或“指令计数器”。

JVM 内存结构中的程序计数器

核心特性:

  1. 极小的内存空间:PC 寄存器是 JVM 内存中占用空间最小的一块区域,几乎可以忽略不计。
  2. 极快的访问速度:由于其实现方式(可能是直接关联 CPU 寄存器或高速缓存),它是 JVM 中运行速度最快的存储区域。
  3. 线程私有 (Thread-Private):这是 PC 寄存器最重要的特性之一。JVM 支持多线程执行,每个 Java 线程在创建时都会拥有自己独立的程序计数器。各个线程的 PC 寄存器互不影响,独立工作。
  4. 生命周期与线程一致:PC 寄存器的生命周期与它所属的线程保持一致,随线程的创建而创建,随线程的结束而销毁。
  5. 存储内容:
    • 执行 Java 方法时:PC 寄存器存储的是当前线程正在执行的 Java 方法的虚拟机字节码指令的地址(相对于方法开头的偏移量)。
    • 执行 Native 方法时:如果当前线程正在执行的是本地(Native)方法(通常由 C/C++ 实现),那么 PC 寄存器的值是未指定值(Undefined)。因为 Native 方法的执行不由 JVM 字节码解释器直接管理。
  6. 程序控制流指示器:PC 寄存器是实现程序控制流(如顺序执行、选择、循环、方法跳转、异常处理等)的基础。字节码解释器在工作时,就是通过读取和更新 PC 寄存器的值来确定下一条要执行的字节码指令。
  7. 无 OutOfMemoryError:它是 JVM 运行时数据区中唯一一个在《Java虚拟机规范》中没有规定任何 OutOfMemoryError 情况的区域。其空间需求固定且很小,通常不会耗尽内存。

形象理解

可以将程序计数器想象成:

  • 代码执行的行号指示器或书签,记录着“下一行”要读到哪里。
  • 数据库结果集(ResultSet)中的游标 (Cursor),指向下一条要处理的数据行。
  • 集合(Collection)的迭代器 (Iterator) 中的指针,指向下一个要访问的元素。 它帮助执行引擎准确无误地接续执行。

# 2. 程序计数器的核心作用

PC 寄存器的核心作用是存储指向下一条将要执行的指令的地址。

当 JVM 的执行引擎(特别是字节码解释器)需要执行下一条指令时,它会首先查看当前线程的 PC 寄存器,获取下一条指令的地址,然后根据这个地址去方法区(或代码缓存)中取出指令进行执行。执行完毕后,执行引擎会更新 PC 寄存器的值,使其指向再下一条要执行的指令地址。

执行引擎读取PC寄存器获取下一条指令

这个过程保证了字节码指令能够按照预定的顺序(或者根据跳转、分支指令改变后的顺序)被连续、正确地执行。

# 3. 代码与字节码示例

让我们通过一个简单的 Java 代码示例,看看 PC 寄存器是如何与字节码指令地址关联的。

Java 代码:

// 文件名: PCRegisterTest.java
public class PCRegisterTest {
    public static void main(String[] args) {
        // 将整数 10 赋值给变量 i
        int i = 10;
        // 将整数 20 赋值给变量 j
        int j = 20;
        // 计算 i + j,并将结果赋值给变量 k
        int k = i + j;

        // 创建一个字符串对象 "abc" (这里未被使用,仅作示例)
        String s = "abc";
        // 打印变量 i 的值
        System.out.println(i);
        // 打印变量 k 的值
        System.out.println(k);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

编译后的部分字节码 (使用 javap -c PCRegisterTest.class):

Compiled from "PCRegisterTest.java"
public class PCRegisterTest {
  public PCRegisterTest();
    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          // 将 byte 值 10 推送到操作数栈顶
       2: istore_1                  // 将栈顶 int 值 (10) 存入局部变量表索引 1 (变量 i)
       3: bipush        20          // 将 byte 值 20 推送到操作数栈顶
       5: istore_2                  // 将栈顶 int 值 (20) 存入局部变量表索引 2 (变量 j)
       6: iload_1                   // 从局部变量表索引 1 (i) 加载 int 值到栈顶
       7: iload_2                   // 从局部变量表索引 2 (j) 加载 int 值到栈顶
       8: iadd                      // 将栈顶两个 int 值相加,结果压回栈顶
       9: istore_3                  // 将栈顶 int 结果 (30) 存入局部变量表索引 3 (变量 k)
      10: ldc           #2          // String abc (从常量池加载 "abc" 引用到栈顶)
      12: astore        4           // 将栈顶引用存入局部变量表索引 4 (变量 s)
      14: getstatic     #3          // Field java/lang/System.out:Ljava/io/PrintStream; (获取 System.out)
      17: iload_1                   // 从局部变量表索引 1 (i) 加载 int 值到栈顶
      18: invokevirtual #4          // Method java/io/PrintStream.println:(I)V (调用 println(int))
      21: getstatic     #3          // Field java/lang/System.out:Ljava/io/PrintStream; (获取 System.out)
      24: iload_3                   // 从局部变量表索引 3 (k) 加载 int 值到栈顶
      25: invokevirtual #4          // Method java/io/PrintStream.println:(I)V (调用 println(int))
      28: return                    // main 方法返回
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

PC 寄存器的作用体现:

  • 字节码指令左侧的数字(0, 2, 3, 5, 6...)代表指令地址或称为字节码偏移量 (Bytecode Offset)。
  • 当 main 方法开始执行时,当前线程的 PC 寄存器初始值可能是 0。
  • 执行引擎读取 PC 值为 0,执行 bipush 10 指令。
  • 执行完 bipush 10 后,执行引擎会更新 PC 寄存器的值为下一条指令的地址,即 2。
  • 然后读取 PC 值为 2,执行 istore_1 指令。
  • 执行完 istore_1 后,更新 PC 值为 3。
  • 如此循环往复,PC 寄存器就像一个指针,一步步引导着执行引擎执行正确的字节码序列。

PC寄存器指示程序执行步骤

# 4. PC 寄存器在线程切换中的关键作用

现代操作系统大多支持多任务处理,允许多个线程并发(或并行)执行。CPU 通过时间片轮转等调度算法,在极短的时间内快速地在不同线程之间切换执行权。

为什么需要 PC 寄存器来支持线程切换?

  • 执行状态保存:当 CPU 从线程 A 切换到线程 B 时,必须保存线程 A 的当前执行状态,以便将来切换回线程 A 时能够准确地从中断的地方继续执行。这个状态中,最重要的信息之一就是线程 A 下一条应该执行的指令地址。
  • 执行状态恢复:当 CPU 从线程 B 切换回线程 A 时,需要恢复线程 A 保存的状态,特别是要将 PC 寄存器的值设置回线程 A 被中断时记录的下一条指令地址。
  • PC 寄存器是关键:JVM 的字节码解释器(以及 JIT 编译后的代码)正是通过每个线程私有的 PC 寄存器来记录这个关键的“下一条指令地址”。当线程被挂起时,其 PC 寄存器的值(即下一条指令的地址)被保存下来;当线程恢复执行时,执行引擎首先读取该线程的 PC 寄存器,就知道应该从哪里继续执行了。

线程切换时PC寄存器保存和恢复执行位置

如果没有线程私有的 PC 寄存器来精确记录每个线程的执行位置,线程切换后就无法正确恢复执行,程序的逻辑就会混乱。

# 5. 常见面试题解析

# 5.1 为什么 PC 寄存器必须是线程私有的?

核心原因:为了保证多线程并发执行的正确性,特别是线程切换后能准确恢复现场。

详细解释:

  1. 并发执行模型:JVM 允许多个线程同时存在并运行。虽然在单核 CPU 上是宏观并行、微观串行(通过时间片快速切换),在多核 CPU 上可以实现真正的并行,但无论哪种情况,CPU 都需要在不同线程之间切换执行权。
  2. 独立执行流:每个线程都有自己独立的执行路径和逻辑,它们执行的代码序列通常是不同的,或者即使执行相同代码,进度也可能不同。
  3. 线程切换的必然性:操作系统和 JVM 的线程调度机制会导致线程执行被频繁中断和恢复。例如,线程 A 执行了一半,时间片用完,切换到线程 B;稍后线程 B 可能也会被中断,再切换回线程 A。
  4. 状态恢复需求:当一个线程(如线程 A)被重新调度获得 CPU 执行权时,它必须能够从上次被中断的地方准确无误地继续执行,而不是从头开始或从其他线程的位置开始。
  5. PC 寄存器的角色:PC 寄存器存储的就是“下一条要执行的指令地址”。如果所有线程共享一个 PC 寄存器,那么当线程切换时,新的线程会覆盖掉旧线程记录的执行位置,导致旧线程恢复时无法找到正确的执行点。
  6. 线程私有的必要性:因此,必须为每个线程分配一个独立的 PC 寄存器。这样,每个线程都可以独立地记录自己的执行进度,线程切换时,只需要保存和恢复各自 PC 寄存器的值,即可保证执行的连续性和正确性,线程之间不会相互干扰执行状态。

PC寄存器私有性确保独立执行 (上图形象地展示了不同线程的 PC 寄存器记录着各自不同的执行位置,切换时互不影响)

# 5.2 什么是 CPU 时间片?它与 PC 寄存器有什么关系?

CPU 时间片 (Time Slice / Quantum) 是操作系统在进行时间片轮转调度 (Round-Robin Scheduling) 时,分配给每个就绪进程(或线程)允许连续占用 CPU 的最长时间段。

工作机制:

  1. 操作系统维护一个就绪队列,包含所有准备好运行的进程/线程。
  2. 调度器选择队列中的第一个进程/线程,让它在 CPU 上运行。
  3. 同时启动一个定时器,设定时间为该进程/线程的时间片长度。
  4. 如果在时间片结束前,该进程/线程主动放弃 CPU(例如,等待 I/O 或执行完毕),则调度器立即选择下一个就绪进程/线程。
  5. 如果时间片用完,但进程/线程仍在运行,定时器会中断 CPU。操作系统介入,将当前进程/线程放回就绪队列的末尾,然后调度队列中的下一个进程/线程投入运行。

目的:

  • 公平性:确保每个就绪的进程/线程都有机会获得 CPU 时间,避免某个长任务长时间独占 CPU 导致其他任务饿死。
  • 响应性:对于交互式应用,即使有后台计算任务在运行,也能通过快速轮转获得 CPU 时间,保持较好的用户响应。

CPU 时间片轮转示意

与 PC 寄存器的关系:

  • CPU 时间片是导致线程切换发生的主要原因之一。当一个线程的时间片用完,它会被操作系统强制中断。
  • 在中断发生时,操作系统(以及 JVM)需要保存该线程的完整执行上下文,以便将来恢复。这个上下文中必须包含程序计数器(PC 寄存器)的值,因为它指明了线程下一次应该从哪里继续执行。
  • 当该线程再次获得 CPU 时间片时,操作系统/JVM 会恢复其保存的上下文,将 PC 寄存器的值加载回来,CPU 就能从正确的位置继续执行指令。
  • 因此,CPU 时间片机制是 PC 寄存器(特别是线程私有的 PC 寄存器)存在的根本原因和应用场景。没有时间片轮转带来的频繁线程切换,PC 寄存器用于状态恢复的作用就不那么突出了。

核心总结

  1. PC 寄存器:JVM 中用于存储当前线程下一条待执行字节码指令地址的内存区域,是实现程序控制流的基础。
  2. 特性:线程私有、内存占用小、访问速度快、无 OOM 风险。
  3. 关键作用:在多线程环境下,精确记录每个线程的执行位置,确保线程切换后能正确恢复执行。
  4. 线程私有的原因:保证并发执行的正确性,避免线程间执行状态相互干扰。
  5. 与 CPU 时间片的关系:时间片轮转是线程切换的主要原因,而 PC 寄存器是实现线程切换时状态保存与恢复的关键。

深入理解程序计数器有助于我们把握 JVM 执行字节码的基本原理以及多线程并发执行的底层机制。

编辑此页 (opens new window)
上次更新: 2025/04/05, 20:16:54
JVM - 运行时数据区概述及线程
JVM - 虚拟机栈

← JVM - 运行时数据区概述及线程 JVM - 虚拟机栈→

Theme by Vdoing | Copyright © 2019-2025 程序员scholar
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式