第四章 运行时数据区:JVM的内存疆域(上)

65 阅读14分钟

第四章 运行时数据区:JVM的内存疆域(上)

1. 总体介绍JVM内存结构

1.1 HotSpot JVM架构概览

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。

graph TB
    subgraph "JVM运行时数据区"
        subgraph "线程私有区域"
            PC[程序计数器]
            JVMStack[Java虚拟机栈]
            NMS[本地方法栈]
        end
        
        subgraph "线程共享区域"
            Heap[堆内存]
            Method[方法区/元空间]
        end
        
        subgraph "直接内存"
            DirectMem[直接内存]
        end
    end
    
    subgraph "执行引擎"
        Interpreter[解释器]
        JIT[即时编译器]
        GC[垃圾收集器]
    end
    
    subgraph "本地方法接口"
        JNI[JNI接口]
        NativeLib[本地方法库]
    end
    
    PC --> Interpreter
    JVMStack --> Interpreter
    Method --> JIT
    Heap --> GC

1.2 内存区域分类

线程私有区域
  • 程序计数器(Program Counter Register):记录当前线程执行的字节码指令地址
  • Java虚拟机栈(Java Virtual Machine Stack):存储局部变量、操作数栈、方法返回地址等
  • 本地方法栈(Native Method Stack):为虚拟机使用到的Native方法服务
线程共享区域
  • 堆内存(Heap):存储对象实例和数组,是垃圾收集器管理的主要区域
  • 方法区/元空间(Method Area/Metaspace):存储类信息、常量、静态变量等
直接内存
  • 直接内存(Direct Memory):不是虚拟机运行时数据区的一部分,但被频繁使用

1.3 内存结构详细架构

flowchart LR
    subgraph JVM["HotSpot JVM 内存架构"]
        subgraph Heap["堆内存 (Heap)"]
            subgraph Young["新生代 (Young Generation)"]
                Eden["Eden区"]
                S0["Survivor 0"]
                S1["Survivor 1"]
            end
            
            subgraph Old["老年代 (Old Generation)"]
                OldGen["老年代"]
            end
        end
        
        subgraph Method["方法区 (Method Area)"]
            subgraph Meta["元空间 (Metaspace)"]
                ClassMeta["类元信息"]
                ConstPool["常量池"]
            end
        end
        
        subgraph PC["程序计数器 (PC Register)"]
            PC1["线程1 PC"]
            PC2["线程2 PC"]
            PC3["线程N PC"]
        end
        
        subgraph JVMStack["Java虚拟机栈"]
            Stack1["线程1栈"]
            Stack2["线程2栈"]
            Stack3["线程N栈"]
        end
        
        subgraph NativeStack["本地方法栈"]
            NativeStack1["线程1本地栈"]
            NativeStack2["线程2本地栈"]
            NativeStack3["线程N本地栈"]
        end
    end
    
    subgraph Direct["直接内存"]
        DirectMem["NIO、Netty等使用"]
    end
    
    %% 样式定义
    style Heap fill:#ffebee
    style Young fill:#e8f5e8
    style Old fill:#fff3e0
    style Method fill:#e3f2fd
    style Meta fill:#f3e5f5
    style PC fill:#fce4ec
    style JVMStack fill:#e0f2f1
    style NativeStack fill:#f1f8e9
    style Direct fill:#fff8e1

1.4 内存区域特点对比

内存区域线程私有/共享是否会OOM垃圾回收主要存储内容
程序计数器私有字节码指令地址
Java虚拟机栈私有局部变量、操作数栈
本地方法栈私有Native方法信息
堆内存共享对象实例、数组
方法区/元空间共享类信息、常量、静态变量
直接内存共享NIO缓冲区等

2. 程序计数器(Program Counter Register)

2.1 为什么需要程序计数器?

程序计数器是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。

sequenceDiagram
    participant CPU
    participant PC as 程序计数器
    participant Memory as 内存
    participant Executor as 执行引擎
    
    CPU->>PC: 1. 获取下一条指令地址
    PC->>Memory: 2. 根据地址获取指令
    Memory->>Executor: 3. 返回字节码指令
    Executor->>Executor: 4. 执行指令
    Executor->>PC: 5. 更新PC值(+1或跳转地址)
    
    Note over CPU,PC: 循环执行,保证程序连续执行
核心作用
  • 程序控制流的指示器:分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
  • 存储指向下一条指令的地址:执行引擎的字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令
  • 线程切换的关键:CPU切换线程时,需要记录每个线程的执行位置
为什么执行native方法时是undefined?

任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址;或者,如果是在执行native方法,则是未指定值(undefined)。

原因:native本地方法大多是通过C实现,并未编译成需要执行的字节码指令,所以在计数器中当然是空(undefined)。

2.2 程序计数器工作原理示例

Java代码示例
public int test() {
    int x = 0;  // 指令地址: 0-1
    int y = 1;  // 指令地址: 2-3  
    return x + y; // 指令地址: 4-7
}
对应的字节码
public int test();
    descriptor: ()I
    flags: ACC_PUBLIC
    
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_0     // 将常量0压入操作数栈
         1: istore_1     // 将栈顶值存储到局部变量表索引1(x)
         2: iconst_1     // 将常量1压入操作数栈
         3: istore_2     // 将栈顶值存储到局部变量表索引2(y)
         4: iload_1      // 将局部变量表索引1的值压入栈(x)
         5: iload_2      // 将局部变量表索引2的值压入栈(y)
         6: iadd         // 执行整数加法
         7: ireturn      // 返回整数值
程序计数器执行流程
flowchart TD
    Start([开始执行方法]) --> PC0[PC = 0: iconst_0]
    PC0 --> PC1[PC = 1: istore_1]
    PC1 --> PC2[PC = 2: iconst_1]
    PC2 --> PC3[PC = 3: istore_2]
    PC3 --> PC4[PC = 4: iload_1]
    PC4 --> PC5[PC = 5: iload_2]
    PC5 --> PC6[PC = 6: iadd]
    PC6 --> PC7[PC = 7: ireturn]
    PC7 --> End([方法执行完毕])
    
    style Start fill:#e1f5fe
    style End fill:#e8f5e8
    style PC0 fill:#fff3e0
    style PC1 fill:#fff3e0
    style PC2 fill:#fff3e0
    style PC3 fill:#fff3e0
    style PC4 fill:#fff3e0
    style PC5 fill:#fff3e0
    style PC6 fill:#fff3e0
    style PC7 fill:#fff3e0

2.3 程序计数器的基本特征

命名来源

JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。

注意:这里并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。

核心特点
  1. 内存空间小:几乎可以忽略不记,是运行速度最快的存储区域
  2. 不需要扩容:不会随着程序的运行需要更大的空间
  3. 线程私有:每个线程都有它自己的程序计数器,生命周期与线程保持一致
  4. 唯一不会OOM的区域:在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域
graph LR
    subgraph "多线程环境"
        subgraph "线程1"
            PC1[程序计数器1]
            Stack1[虚拟机栈1]
        end
        
        subgraph "线程2"
            PC2[程序计数器2]
            Stack2[虚拟机栈2]
        end
        
        subgraph "线程N"
            PCN[程序计数器N]
            StackN[虚拟机栈N]
        end
    end
    
    subgraph "共享区域"
        Heap[堆内存]
        Method[方法区]
    end
    
    PC1 -.-> Heap
    PC2 -.-> Heap
    PCN -.-> Heap
    Stack1 -.-> Method
    Stack2 -.-> Method
    StackN -.-> Method

2.4 关键问题解答

PC寄存器存储字节码指令地址有什么用?

核心作用

  • CPU需要不停的切换各个线程,切换回来以后需要知道接着从哪开始继续执行
  • JVM的字节码解释器需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
sequenceDiagram
    participant OS as 操作系统
    participant Thread1 as 线程1
    participant Thread2 as 线程2
    participant CPU
    
    Thread1->>CPU: 执行指令A (PC=100)
    Note over CPU: 时间片到期
    CPU->>Thread1: 保存状态 (PC=101)
    
    OS->>CPU: 切换到线程2
    Thread2->>CPU: 执行指令B (PC=200)
    Note over CPU: 时间片到期
    CPU->>Thread2: 保存状态 (PC=201)
    
    OS->>CPU: 切换回线程1
    Thread1->>CPU: 从PC=101继续执行
    
    Note over Thread1,CPU: 无缝恢复执行,保证程序连续性
PC寄存器为什么被设定为线程私有的?

原因分析

  • 多线程在特定时间段内只会执行其中某一个线程的方法
  • CPU会不停地做任务切换,导致经常中断或恢复
  • 解决方案:为每一个线程都分配一个PC寄存器,各个线程之间便可以进行独立计算,不会出现相互干扰

2.6 程序计数器总结

mindmap
  root((程序计数器))
    作用
      指示器
        程序控制流
        分支循环跳转
        异常处理
      存储地址
        下一条指令地址
        字节码指令位置
      线程切换
        保存执行位置
        恢复执行状态
    特征
      内存
        空间很小
        运行最快
        不会扩容
      线程
        线程私有
        生命周期一致
        独立计算
      异常
        唯一不会OOM
        规范未定义错误
    实现
      Java方法
        存储JVM指令地址
        字节码行号指示
      Native方法
        undefined
        C语言实现
        无字节码指令

关键要点

  1. 程序计数器是JVM运行时数据区中唯一不会发生OutOfMemoryError的区域
  2. 它是线程私有的,每个线程都有独立的程序计数器
  3. 执行Java方法时存储字节码指令地址,执行Native方法时为undefined
  4. 是CPU线程切换和程序连续执行的关键保障机制

3. Java虚拟机栈(Java Virtual Machine Stack)

3.1 概述

栈管运行,堆管存储

Java虚拟机栈是Java程序运行的核心组件,它主管Java程序的运行,保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。

核心作用

  • 局部变量存储:基本数据类型变量 vs 引用类型变量(类、数组、接口)
  • 方法调用管理:方法的调用和返回过程
  • 程序执行控制:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据
flowchart LR
    subgraph "内存职责分工"
        Stack[虚拟机栈]
        Heap[堆内存]
    end
    
    Stack -->|管理| A[程序运行]
    Stack -->|存储| B[局部变量]
    Stack -->|控制| C[方法调用]
    
    Heap -->|管理| D[数据存储]
    Heap -->|存储| E[对象实例]
    Heap -->|控制| F[内存分配]
    
    style Stack fill:#e3f2fd
    style Heap fill:#f3e5f5

3.2 虚拟机栈的特性

垃圾回收与异常

关键特点

  • 不存在GC:虚拟机栈不需要垃圾回收
  • 存在OOM:可能出现StackOverflowError和OutOfMemoryError
flowchart TD
    A[Java虚拟机栈] --> B{栈大小类型}
    
    B -->|固定大小| C[StackOverflowError]
    B -->|动态扩展| D{内存是否足够}
    
    C --> E[线程请求栈深度超过允许深度]
    
    D -->|足够| F[正常扩展]
    D -->|不足| G[OutOfMemoryError]
    
    G --> H[扩展时无法申请足够内存]
    G --> I[创建新线程时内存不足]
    
    style C fill:#ffcdd2
    style G fill:#ffcdd2
    style F fill:#c8e6c9
栈溢出场景分析
  • 什么情况下会发生栈内存溢出?
  • 栈存在内存溢出吗?
  • 说一下什么情况发生栈溢出?

主要原因

  1. 局部数组过大:函数内部的数组过大时,可能导致栈溢出
  2. 递归调用层次太多:递归函数压栈次数过多时,导致栈溢出
// 栈溢出示例
public class StackOverflowExample {
    // 无限递归导致栈溢出
    public void infiniteRecursion() {
        infiniteRecursion(); // StackOverflowError
    }
    
    // 大数组导致栈溢出
    public void largeArray() {
        int[] largeArray = new int[Integer.MAX_VALUE]; // 可能导致栈溢出
    }
}

3.3 栈大小配置

参数设置

如何设置栈内存大小?

  • 参数-Xss size(等价于 -XX:ThreadStackSize
  • 默认值:512k-1024k,取决于操作系统
  • 影响:栈的大小直接决定了函数调用的最大可达深度
# 栈大小设置示例
-Xss1m      # 设置栈大小为1MB
-Xss512k    # 设置栈大小为512KB
-Xss2048k   # 设置栈大小为2MB
各版本默认值对比
JDK版本默认栈大小平台
JDK 5.0之前256k所有平台
JDK 5.0之后1024kLinux/Mac/Windows
当前显示0(实际1024k)Windows

重要提醒

  • 设置的栈空间值过大,会导致系统可用于创建线程的数量减少
  • 一般一个进程中通常有300-500个线程

3.4 栈的单位:栈帧(Stack Frame)

栈帧基本概念

每个线程都有自己的栈,栈中的数据都是以 栈帧(Stack Frame) 的格式存在。

方法与栈帧的关系
sequenceDiagram
    participant Thread as 线程
    participant Stack as 虚拟机栈
    participant Frame1 as 栈帧1(method1)
    participant Frame2 as 栈帧2(method2)
    participant Frame3 as 栈帧3(method3)
    
    Thread->>Stack: 调用method1()
    Stack->>Frame1: 创建栈帧1
    Note over Frame1: 当前栈帧
    
    Frame1->>Frame2: 调用method2()
    Note over Frame2: 新的当前栈帧
    
    Frame2->>Frame3: 调用method3()
    Note over Frame3: 最新当前栈帧
    
    Frame3->>Frame2: method3()返回
    Frame2->>Frame1: method2()返回
    Frame1->>Stack: method1()返回
    Stack->>Thread: 执行完成

核心概念

  • 栈帧:一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息
  • 当前栈帧(Current Frame):栈顶栈帧,当前正在执行的方法的栈帧
  • 当前方法(Current Method):与当前栈帧相对应的方法
  • 当前类(Current Class):定义当前方法的类
栈的FILO原理
flowchart TD
    subgraph "栈操作过程"
        A[方法调用] -->|压栈| B[创建栈帧]
        B --> C[执行方法]
        C -->|出栈| D[销毁栈帧]
        D --> E[返回结果]
    end
    
    subgraph "FILO示例"
        F[栈底: main方法] 
        G[method1栈帧]
        H[method2栈帧]
        I[栈顶: method3栈帧]
        
        F --> G --> H --> I
    end
    
    style I fill:#ffeb3b
    style A fill:#e3f2fd
    style E fill:#c8e6c9

JVM对Java栈的操作

  • 压栈(入栈):每个方法执行时创建栈帧
  • 出栈:方法执行结束后销毁栈帧
  • 遵循原则:"先进后出"/"后进先出"(FILO)

重要特性

  • 不同线程中的栈帧不允许相互引用
  • 方法返回时,当前栈帧传回执行结果给前一个栈帧
  • Java方法有两种返回方式:正常返回(return指令)和异常返回

3.5 栈帧内部结构详解

栈帧组成概览
classDiagram
    class StackFrame {
        +LocalVariableTable 局部变量表
        +OperandStack 操作数栈
        +DynamicLinking 动态链接
        +ReturnAddress 方法返回地址
        +AdditionalInfo 附加信息
    }
    
    class LocalVariableTable {
        +Slot[] slots
        +int maxLocals
        +storeParameter()
        +storeLocalVariable()
    }
    
    class OperandStack {
        +Object[] stack
        +int maxStack
        +push()
        +pop()
    }
    
    class DynamicLinking {
        +ConstantPoolReference reference
        +resolveSymbolicReference()
    }
    
    class ReturnAddress {
        +int pcValue
        +ExceptionTable exceptionTable
    }
    
    StackFrame *-- LocalVariableTable
    StackFrame *-- OperandStack
    StackFrame *-- DynamicLinking
    StackFrame *-- ReturnAddress

3.6 局部变量表(Local Variables)

基本概念

局部变量表也被称为局部变量数组或本地变量表,是栈帧中最重要的组成部分之一。

核心特点

  • 定义:一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量
  • 数据类型:8种基本数据类型、对象引用(reference)、returnAddress类型
  • 容量确定:在编译期确定,保存在方法的Code属性的maximum local variables数据项中
  • 运行期不变:方法运行期间不会改变局部变量表的大小
flowchart LR
    subgraph "局部变量表结构"
        A[索引0] -->|this引用| B[实例方法]
        A -->|第一个参数| C[静态方法]
        D[索引1] --> E[方法参数1]
        F[索引2] --> G[方法参数2]
        H[索引n] --> I[局部变量]
    end
    
    subgraph "编译期信息"
        J[maximum local variables]
        K[LocalVariableTable]
        L[作用域范围]
    end
    
    style A fill:#ffeb3b
    style J fill:#e3f2fd
线程安全性

重要结论:局部变量表不存在线程安全问题

原因:局部变量表建立在线程的栈上,是线程的私有数据

Slot槽机制详解

Slot(变量槽) 是局部变量表的最基本存储单元:

flowchart LR
    subgraph "Slot占用规则"
        A[32位类型] --> B[占用1个Slot]
        C[64位类型] --> D[占用2个Slot]
    end
    
    subgraph "具体类型映射"
        E[byte/short/char] --> F[转换为int占用1个Slot]
        G[boolean] --> H[转换为int占用1个Slot]
        I[int/float/reference] --> J[占用1个Slot]
        K[long/double] --> L[占用2个Slot]
    end
    
    style B fill:#c8e6c9
    style D fill:#ffcdd2
    style J fill:#c8e6c9
    style L fill:#ffcdd2

Slot访问机制

  • JVM为每个Slot分配访问索引
  • 通过索引访问局部变量表中的指定局部变量值
  • 访问64bit变量时,只需使用前一个索引
  • 实例方法中,this引用存放在index为0的slot处
Slot重复利用机制

栈帧中的局部变量表中的槽位是可以重用的,实现资源节省:

public class SlotReuseExample {
    public void method1() {
        int a = 0;  // 占用slot 1
        System.out.println(a);
        int b = 0;  // 占用slot 2
    }

    public void method2() {
        {
            int a = 0;  // 占用slot 1
            System.out.println(a);
        }
        // 此时的b会复用a的槽位
        int b = 0;  // 复用slot 1
    }
}
sequenceDiagram
    participant Scope1 as 作用域1
    participant Slot1 as Slot[1]
    participant Scope2 as 作用域2
    
    Scope1->>Slot1: 变量a占用
    Note over Slot1: a的生命周期
    Scope1->>Slot1: a作用域结束
    
    Scope2->>Slot1: 变量b复用
    Note over Slot1: b的生命周期
    Scope2->>Slot1: b作用域结束
静态变量与局部变量对比
特性静态变量局部变量
初始化时机准备阶段(零值)+ 初始化阶段(代码值)必须人为初始化
默认值有系统默认值无默认值,必须赋值
生命周期类的生命周期方法调用期间
线程安全需要考虑天然线程安全
// 错误示例:局部变量未初始化
public void test() {
    int i;  // 编译错误:未初始化
    System.out.println(i);
}
与GC Roots的关系

重要概念:局部变量表中的变量是重要的垃圾回收根节点

flowchart TD
    A[GC Roots] --> B[局部变量表引用]
    A --> C[静态变量引用]
    A --> D[JNI引用]
    A --> E[活动线程]
    
    B --> F[直接引用对象]
    B --> G[间接引用对象]
    
    F --> H[存活对象]
    G --> H
    
    style H fill:#c8e6c9
    style A fill:#ffeb3b

GC规则:只要被局部变量表中直接或间接引用的对象都不会被回收

3.7 操作数栈(Operand Stack)

基本概念

Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。

核心特点

  • 别名:表达式栈(Expression Stack)
  • 结构:后进先出(LIFO)
  • 初始状态:方法刚开始执行时,操作数栈是空的
  • 深度确定:编译期确定,保存在方法的Code属性中的max_stack值
flowchart TD
    subgraph "操作数栈工作流程"
        A[字节码指令] --> B{指令类型}
        
        B -->|压栈指令| C[将值压入栈]
        B -->|出栈指令| D[从栈中取值]
        B -->|计算指令| E[取出操作数计算]
        
        C --> F[栈深度+1]
        D --> G[栈深度-1]
        E --> H[将结果压入栈]
        
        H --> F
    end
    
    style C fill:#e3f2fd
    style D fill:#fff3e0
    style E fill:#f3e5f5
数据类型与栈深度
数据类型占用栈深度
32bit类型1个栈单位
64bit类型2个栈单位
int/float/reference1个栈单位
long/double2个栈单位
操作数栈工作示例
public void testAddOperation() {
    byte i = 15;
    int j = 8;
    int k = i + j;
}

对应字节码:

public void testAddOperation();
    Code:
    0: bipush        15    // 将15压入操作数栈
    2: istore_1           // 将栈顶值存储到局部变量表索引1(i)
    3: bipush        8     // 将8压入操作数栈
    5: istore_2           // 将栈顶值存储到局部变量表索引2(j)
    6: iload_1            // 将局部变量表索引1的值压入栈(i)
    7: iload_2            // 将局部变量表索引2的值压入栈(j)
    8: iadd               // 执行整数加法
    9: istore_3           // 将结果存储到局部变量表索引3(k)
    10: return            // 方法返回
详细执行过程与状态变化

初始状态

flowchart LR
    subgraph "局部变量表"
        LVT0["索引0: this"]
        LVT1["索引1: 未定义"]
        LVT2["索引2: 未定义"]
        LVT3["索引3: 未定义"]
    end
    
    subgraph "操作数栈"
        OS["空栈 []"]
    end
    
    style LVT0 fill:#e3f2fd
    style OS fill:#fff3e0

指令1: bipush 15

flowchart LR
    subgraph "局部变量表"
        LVT0["索引0: this"]
        LVT1["索引1: 未定义"]
        LVT2["索引2: 未定义"]
        LVT3["索引3: 未定义"]
    end
    
    subgraph "操作数栈"
        OS["栈顶: 15"]
    end
    
    style LVT0 fill:#e3f2fd
    style OS fill:#c8e6c9

指令2: istore_1

flowchart LR
    subgraph "局部变量表"
        LVT0["索引0: this"]
        LVT1["索引1: 15"]
        LVT2["索引2: 未定义"]
        LVT3["索引3: 未定义"]
    end
    
    subgraph "操作数栈"
        OS["空栈 []"]
    end
    
    style LVT0 fill:#e3f2fd
    style LVT1 fill:#c8e6c9
    style OS fill:#fff3e0

指令3: bipush 8

flowchart LR
    subgraph "局部变量表"
        LVT0["索引0: this"]
        LVT1["索引1: 15"]
        LVT2["索引2: 未定义"]
        LVT3["索引3: 未定义"]
    end
    
    subgraph "操作数栈"
        OS["栈顶: 8"]
    end
    
    style LVT0 fill:#e3f2fd
    style LVT1 fill:#c8e6c9
    style OS fill:#c8e6c9

指令4: istore_2

flowchart LR
    subgraph "局部变量表"
        LVT0["索引0: this"]
        LVT1["索引1: 15"]
        LVT2["索引2: 8"]
        LVT3["索引3: 未定义"]
    end
    
    subgraph "操作数栈"
        OS["空栈 []"]
    end
    
    style LVT0 fill:#e3f2fd
    style LVT1 fill:#c8e6c9
    style LVT2 fill:#c8e6c9
    style OS fill:#fff3e0

指令5: iload_1

flowchart LR
    subgraph "局部变量表"
        LVT0["索引0: this"]
        LVT1["索引1: 15"]
        LVT2["索引2: 8"]
        LVT3["索引3: 未定义"]
    end
    
    subgraph "操作数栈"
        OS["栈顶: 15"]
    end
    
    style LVT0 fill:#e3f2fd
    style LVT1 fill:#ffeb3b
    style LVT2 fill:#c8e6c9
    style OS fill:#c8e6c9

指令6: iload_2

flowchart LR
    subgraph "局部变量表"
        LVT0["索引0: this"]
        LVT1["索引1: 15"]
        LVT2["索引2: 8"]
        LVT3["索引3: 未定义"]
    end
    
    subgraph "操作数栈"
        OS1["栈底: 15"]
        OS2["栈顶: 8"]
        OS1 --> OS2
    end
    
    style LVT0 fill:#e3f2fd
    style LVT1 fill:#c8e6c9
    style LVT2 fill:#ffeb3b
    style OS1 fill:#c8e6c9
    style OS2 fill:#c8e6c9

指令7: iadd

flowchart LR
    subgraph "局部变量表"
        LVT0["索引0: this"]
        LVT1["索引1: 15"]
        LVT2["索引2: 8"]
        LVT3["索引3: 未定义"]
    end
    
    subgraph "操作数栈"
        OS["栈顶: 23"]
        Note["15 + 8 = 23"]
        OS -.-> Note
    end
    
    style LVT0 fill:#e3f2fd
    style LVT1 fill:#c8e6c9
    style LVT2 fill:#c8e6c9
    style OS fill:#f3e5f5
    style Note fill:#fff3e0

指令8: istore_3

flowchart LR
    subgraph "局部变量表"
        LVT0["索引0: this"]
        LVT1["索引1: 15"]
        LVT2["索引2: 8"]
        LVT3["索引3: 23"]
    end
    
    subgraph "操作数栈"
        OS["空栈 []"]
    end
    
    style LVT0 fill:#e3f2fd
    style LVT1 fill:#c8e6c9
    style LVT2 fill:#c8e6c9
    style LVT3 fill:#f3e5f5
    style OS fill:#fff3e0
执行流程总结
sequenceDiagram
    participant PC as 程序计数器
    participant LVT as 局部变量表
    participant OS as 操作数栈
    participant Inst as 字节码指令
    
    PC->>Inst: PC=0, 获取指令
    Inst->>OS: bipush 15
    Note over OS: [15]
    
    PC->>Inst: PC=2, 获取指令
    OS->>LVT: istore_1 (i=15)
    Note over LVT: [this, 15, -, -]
    Note over OS: []
    
    PC->>Inst: PC=3, 获取指令
    Inst->>OS: bipush 8
    Note over OS: [8]
    
    PC->>Inst: PC=5, 获取指令
    OS->>LVT: istore_2 (j=8)
    Note over LVT: [this, 15, 8, -]
    Note over OS: []
    
    PC->>Inst: PC=6, 获取指令
    LVT->>OS: iload_1
    Note over OS: [15]
    
    PC->>Inst: PC=7, 获取指令
    LVT->>OS: iload_2
    Note over OS: [15, 8]
    
    PC->>Inst: PC=8, 获取指令
    OS->>OS: iadd (15+8)
    Note over OS: [23]
    
    PC->>Inst: PC=9, 获取指令
    OS->>LVT: istore_3 (k=23)
    Note over LVT: [this, 15, 8, 23]
    Note over OS: []
    
    PC->>Inst: PC=10, 获取指令
    Inst->>PC: return (方法结束)

3.8 动态链接(Dynamic Linking)

基本概念

每个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,支持当前方法的代码实现动态链接。

核心作用:将符号引用转换为调用方法的直接引用

flowchart TD
    subgraph "编译期"
        A[Java源码] --> B[字节码文件]
        B --> C[常量池中的符号引用]
    end
    
    subgraph "运行期"
        D[动态链接] --> E[符号引用解析]
        E --> F[直接引用]
        F --> G[方法调用]
    end
    
    C --> D
    
    style C fill:#fff3e0
    style F fill:#c8e6c9
链接过程示例
public void testGetSum(){
    int i = getSum();  // 调用getSum方法
    int j = 10;
}

链接过程

  1. 编译期:方法调用被保存为符号引用
  2. 运行期:动态链接将符号引用转换为直接引用
  3. 执行期:通过直接引用调用具体方法
sequenceDiagram
    participant SF as 当前栈帧
    participant CP as 运行时常量池
    participant DL as 动态链接
    participant Method as 目标方法
    
    SF->>CP: 查找符号引用
    CP->>DL: 返回符号引用信息
    DL->>DL: 解析符号引用
    DL->>Method: 获取直接引用
    Method->>SF: 返回方法地址
    SF->>Method: 调用方法

3.9 方法返回地址(Return Address)

基本概念

存放调用该方法的PC寄存器的值,确保方法执行完成后能正确返回到调用位置。

方法退出方式
flowchart TD
    A[方法执行] --> B{退出方式}
    
    B -->|正常完成| C[正常完成出口]
    B -->|异常退出| D[异常完成出口]
    
    C --> E[return指令]
    C --> F[返回值传递]
    C --> G[PC寄存器值作为返回地址]
    
    D --> H[未处理异常]
    D --> I[异常表确定返回地址]
    D --> J[无返回值]
    
    style C fill:#c8e6c9
    style D fill:#ffcdd2
返回指令类型
返回值类型返回指令
boolean/byte/char/short/intireturn
longlreturn
floatfreturn
doubledreturn
referenceareturn
voidreturn
方法退出过程
sequenceDiagram
    participant Caller as 调用者方法
    participant Current as 当前方法
    participant Stack as 虚拟机栈
    
    Caller->>Current: 方法调用
    Note over Current: 保存调用者PC值
    
    Current->>Current: 方法执行
    
    alt 正常退出
        Current->>Caller: 返回值
        Current->>Stack: 栈帧出栈
        Stack->>Caller: 恢复调用者状态
    else 异常退出
        Current->>Stack: 栈帧出栈
        Stack->>Caller: 异常传播
    end
    
    Note over Caller: 继续执行

退出时需要恢复的状态

  • 上层方法的局部变量表
  • 操作数栈
  • 将返回值压入调用者栈帧的操作数栈
  • 设置PC寄存器值等

3.10 附加信息

栈帧中还允许携带与Java虚拟机实现相关的附加信息:

  • 调试信息:支持程序调试
  • 性能监控信息:方法执行时间、调用次数等
  • 异常处理表:异常处理的跳转信息

3.11 问题小结与拓展

常见问题解答

问题一:栈溢出的情况?

  • 栈溢出:StackOverflowError
  • 典型场景:无限递归调用
  • 解决方案:调整-Xss参数或优化代码结构

问题二:调整栈大小,就能保证不出现溢出吗?

  • 答案:不能
  • 原因:只能减少溢出可能性,栈大小不能无限扩大

问题三:分配的栈内存越大越好吗?

  • 答案:不是
  • 原因:栈越大,能创建的线程数量越少

问题四:垃圾回收是否会涉及到虚拟机栈?

  • 答案:不会
  • 原因:栈只涉及压栈和出栈,可能存在栈溢出,不存在垃圾回收

问题五:方法中定义的局部变量是否线程安全?

public class LocalVariableThreadSafe {
    // 线程安全:局部变量在方法内部创建和销毁
    public static void method1() {
        StringBuilder s1 = new StringBuilder();
        s1.append("a");
        s1.append("b");
    }

    // 线程不安全:参数可能被多个线程共享
    public static void method2(StringBuilder stringBuilder) {
        stringBuilder.append("a");
        stringBuilder.append("b");
    }

    // 线程不安全:返回值可能被多个线程共享
    public static StringBuilder method3() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("a");
        stringBuilder.append("b");
        return stringBuilder;
    }

    // 线程安全:返回新创建的String对象
    public static String method4() {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("a");
        stringBuilder.append("b");
        return stringBuilder.toString();
    }
}

结论:如果局部变量在内部产生并在内部消亡,那就是线程安全的

线程安全判断流程
flowchart TD
    A[局部变量] --> B{是否逃逸出方法}
    
    B -->|否| C[线程安全]
    B -->|是| D{逃逸方式}
    
    D -->|作为参数传入| E[可能线程不安全]
    D -->|作为返回值| F[可能线程不安全]
    D -->|赋值给实例变量| G[线程不安全]
    
    style C fill:#c8e6c9
    style E fill:#fff3e0
    style F fill:#fff3e0
    style G fill:#ffcdd2

3.12 虚拟机栈总结

mindmap
  root((虚拟机栈))
    概述
      栈管运行
        程序执行控制
        方法调用管理
        局部变量存储
      堆管存储
        对象实例存储
        内存分配管理
        垃圾回收管理
    特性
      异常
        StackOverflowError
        OutOfMemoryError
        不存在GC
      配置
        Xss参数
        默认1024k
        影响线程数量
    栈帧结构
      局部变量表
        Slot槽机制
        线程安全
        GC Roots
      操作数栈
        LIFO结构
        字节码执行
        栈顶缓存
      动态链接
        符号引用
        直接引用
        运行时解析
      返回地址
        正常退出
        异常退出
        状态恢复

关键要点

  1. 虚拟机栈是线程私有的,每个线程都有独立的栈空间
  2. 栈帧是方法调用的基本单位,包含局部变量表、操作数栈等核心组件
  3. 局部变量表存储方法参数和局部变量,是GC Roots的重要组成部分
  4. 操作数栈是字节码指令的工作区,采用LIFO结构
  5. 动态链接负责将符号引用转换为直接引用
  6. 方法返回地址确保方法调用的正确返回

下一章预告:我们将探讨本地方法栈的特性与工作机制,以及它与Java虚拟机栈的区别和联系。