知识模块
☕ Java 知识模块
四、JVM 深入理解
运行时数据区

运行时数据区

JVM 运行时数据区是 Java 程序运行时的内存区域,分为线程私有和线程共享两部分。

整体架构

┌─────────────────────────────────────────────────────────────┐
│                    JVM 运行时数据区                           │
├─────────────┬─────────────┬─────────────┬─────────────────┤
│  程序计数器   │   虚拟机栈   │  本地方法栈  │      堆          │
│  (线程私有)   │  (线程私有)  │  (线程私有)  │   (线程共享)     │
├─────────────┴─────────────┴─────────────┼─────────────────┤
│                  方法区                   │ 运行时常量池      │
│               (线程共享)                  │ (方法区一部分)    │
└─────────────────────────────────────────┴─────────────────┘

一、程序计数器 (Program Counter)

概述

程序计数器是一块较小的内存空间,记录当前线程执行的字节码行号。

特点

特性说明
线程私有每个线程都有独立的 PC
无 OOM唯一没有 OutOfMemoryError 的区域
存储内容字节码指令地址 / undefined(本地方法)

工作原理

// Java 代码
int a = 1;
int b = 2;
int c = a + b;
 
// 字节码
0: iconst_1      // PC = 0
1: istore_1      // PC = 1
2: iconst_2      // PC = 2
3: istore_2      // PC = 3
4: iload_1       // PC = 4
5: iload_2       // PC = 5
6: iadd          // PC = 6
7: istore_3      // PC = 7

为什么需要 PC?

多线程环境下,CPU 需要切换线程。PC 记录每个线程的执行位置,恢复时继续执行。

二、虚拟机栈 (Java Virtual Machine Stack)

概述

虚拟机栈描述 Java 方法执行的内存模型:每个方法执行时创建一个栈帧。

栈帧结构

┌────────────────────────┐
│        栈帧            │
├────────────────────────┤
│  局部变量表             │
│  (Local Variables)     │
├────────────────────────┤
│  操作数栈               │
│  (Operand Stack)       │
├────────────────────────┤
│  动态链接               │
│  (Dynamic Linking)     │
├────────────────────────┤
│  方法返回地址           │
│  (Return Address)      │
└────────────────────────┘

局部变量表

存储方法参数和局部变量:

public int add(int a, int b) {
    int c = a + b;
    return c;
}
 
// 局部变量表
// slot 0: this(非静态方法)
// slot 1: a
// slot 2: b
// slot 3: c

注意:局部变量表没有初始化值,必须显式初始化后才能使用。

操作数栈

方法执行过程中的计算工作区:

int c = a + b;
 
// 字节码执行过程
iload_1    // 将 a 压入操作数栈
iload_2    // 将 b 压入操作数栈
iadd       // 弹出两个值相加,结果压栈
istore_3   // 弹出结果存入局部变量表

异常

异常触发条件
StackOverflowError栈深度超过限制
OutOfMemoryError栈扩展时内存不足

栈配置

# 设置每个线程栈大小
-Xss256k

三、本地方法栈 (Native Method Stack)

概述

为 Native 方法服务的栈,与虚拟机栈类似。

特点

  • 调用 C/C++ 实现的本地方法
  • HotSpot 中与虚拟机栈合二为一
  • 同样可能抛出 StackOverflowError 和 OutOfMemoryError

示例

// Object.hashCode() 是本地方法
public native int hashCode();
 
// Thread.start() 调用本地方法
private native void start0();

四、堆 (Heap)

概述

堆是 JVM 最大的内存区域,存储所有对象实例和数组。

特点

特性说明
线程共享所有线程共享堆内存
GC 主战场垃圾收集的主要区域
可扩展-Xms 和 -Xmx 控制

堆内存结构

┌─────────────────────────────────────────────┐
│                   堆 (Heap)                  │
├─────────────────────┬───────────────────────┤
│    新生代 (Young)    │     老年代 (Old)       │
├─────────┬─────────┬─┴───────────────────────┤
│   Eden  │ Survivor │                         │
│         │  S0  S1  │                         │
│  8 : 1  :   1      │                         │
└─────────┴─────────┴─────────────────────────┘

对象分配流程

新对象

Eden 区分配 → 空间足够?
   ↓ 否
触发 Minor GC

存活对象 → Survivor 区 → 年龄 +1

年龄 ≥ 15 (默认) → 晋升老年代

大对象直接进老年代

// 大对象:需要大量连续内存的对象(长数组、大字符串)
// -XX:PretenureSizeThreshold 设置阈值
 
byte[] bigArray = new byte[4 * 1024 * 1024];  // 4MB,可能直接进老年代

堆配置

# 初始堆大小
-Xms512m
 
# 最大堆大小
-Xmx1024m
 
# 新生代比例
-XX:NewRatio=2        # 老年代:新生代 = 2:1
 
# Eden:Survivor 比例
-XX:SurvivorRatio=8   # Eden:S0:S1 = 8:1:1

OOM 场景

// 堆溢出
List<byte[]> list = new ArrayList<>();
while (true) {
    list.add(new byte[1024 * 1024]);  // 持续创建大对象
}
// java.lang.OutOfMemoryError: Java heap space

五、方法区 (Method Area)

概述

存储类信息、常量、静态变量、即时编译器生成的代码等。

特点

特性说明
线程共享所有线程共享
逻辑上是堆的一部分HotSpot 中称为"非堆"
不同实现JDK7: 永久代 / JDK8+: 元空间

永久代 vs 元空间

特性永久代 (JDK7-)元空间 (JDK8+)
位置JVM 堆内存本地内存
大小限制受堆大小限制受系统内存限制
OOMPermGen spaceMetaspace
配置-XX:PermSize-XX:MetaspaceSize

存储内容

方法区
├── 类信息
│   ├── 类名、访问修饰符
│   ├── 父类、接口
│   ├── 字段、方法
│   └── 方法字节码
├── 运行时常量池
├── 静态变量
└── 即时编译代码

方法区配置

# JDK8+ 元空间
-XX:MetaspaceSize=128m      # 初始大小
-XX:MaxMetaspaceSize=256m   # 最大大小

六、运行时常量池 (Runtime Constant Pool)

概述

运行时常量池是方法区的一部分,存储编译期生成的字面量和符号引用。

常量池内容

类型示例
字面量数字、字符串
符号引用类/方法/字段的引用

String 常量池

// 字符串常量池示例
String s1 = "hello";           // 常量池
String s2 = "hello";           // 复用常量池中的对象
String s3 = new String("hello"); // 堆中新建对象
 
System.out.println(s1 == s2);  // true
System.out.println(s1 == s3);  // false
System.out.println(s1 == s3.intern()); // true
 
// intern() 方法:将字符串加入常量池并返回引用

常量池溢出

// JDK6 会 OOM
// JDK7+ 常量池在堆中,受堆大小限制
List<String> list = new ArrayList<>();
int i = 0;
while (true) {
    list.add(String.valueOf(i++).intern());
}

七、直接内存 (Direct Memory)

概述

直接内存不是 JVM 运行时数据区的一部分,而是 NIO 使用的本地内存。

特点

特性说明
非堆内存直接使用系统内存
零拷贝避免 Java 堆与 Native 堆之间复制
受系统限制可能导致 OOM

NIO 示例

// 使用直接内存
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
 
// 底层使用 Unsafe 分配本地内存
// 访问更快,但分配/释放开销更大

配置

# 最大直接内存
-XX:MaxDirectMemorySize=256m

八、对象内存布局

对象结构

┌────────────────────────────────────┐
│           对象头 (Header)           │
├────────────────────────────────────┤
│  Mark Word (8 bytes)               │
│  - 哈希码、锁状态、GC 年龄           │
├────────────────────────────────────┤
│  类型指针 (4/8 bytes)               │
│  - 指向类元数据                     │
├────────────────────────────────────┤
│  实例数据 (Instance Data)           │
│  - 字段值                          │
├────────────────────────────────────┤
│  对齐填充 (Padding)                 │
│  - 保证 8 字节对齐                  │
└────────────────────────────────────┘

对象大小计算

// 64 位 JVM,开启指针压缩
Object obj = new Object();  // 16 字节
// Mark Word: 8 字节
// 类型指针: 4 字节(压缩后)
// 对齐填充: 4 字节
 
// 数组对象额外 4 字节存储长度
int[] arr = new int[10];    // 16 + 40 = 56 字节

九、内存模型对比

区域线程安全OOM 类型存储内容
程序计数器私有字节码行号
虚拟机栈私有StackOverflow / OOM栈帧
本地方法栈私有StackOverflow / OOMNative 栈帧
共享OOM对象实例
方法区共享OOM类信息、常量
直接内存共享OOMNIO 缓冲区

常见面试题

Q1: 栈和堆的区别?

特性
存储内容基本类型、对象引用对象实例
生命周期方法结束自动释放GC 回收
空间大小较小,固定较大,可扩展
碎片

Q2: 为什么 String 要放在常量池?

  1. 节省内存:相同字符串只存一份
  2. 性能优化:常量池查找比堆快
  3. 线程安全:常量池字符串不可变

Q3: 方法区会 OOM 吗?

会。动态生成大量类(如 CGLib、动态代理)可能撑爆方法区。

// CGLib 动态代理生成大量类
while (true) {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(Object.class);
    enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> null);
    enhancer.create();  // 不断生成新类
}
// java.lang.OutOfMemoryError: Metaspace

Q4: 如何排查 OOM?

# 1. 打印 GC 日志
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
 
# 2. OOM 时生成堆转储
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=dump.hprof
 
# 3. 使用工具分析
jmap -histo:live <pid>     # 查看对象统计
jhat dump.hprof            # 分析堆转储
MAT / VisualVM             # 可视化工具

小结

  • 程序计数器:线程私有,记录执行位置
  • 虚拟机栈:存储栈帧,方法调用链
  • 本地方法栈:Native 方法调用
  • 堆:对象实例存储,GC 主战场
  • 方法区:类信息、常量、静态变量
  • 运行时常量池:字面量和符号引用
  • 直接内存:NIO 使用的本地内存