Skip to content
清晨的一缕阳光
返回

JVM 内存模型详解

JVM 内存模型详解

JVM 运行时数据区是 Java 程序执行的基础,理解内存结构对性能优化和问题排查至关重要。

一、整体架构

image-20250803225955497

JVM 运行时数据区分为五大部分:

区域用途线程共享异常
程序计数器记录执行位置私有
Java 虚拟机栈方法调用栈帧私有StackOverflowError / OutOfMemoryError
本地方法栈Native 方法私有StackOverflowError / OutOfMemoryError
对象实例共享OutOfMemoryError
方法区类元数据共享OutOfMemoryError

二、程序计数器(PC Register)

2.1 作用

2.2 特点

每个线程独立

存储当前执行的字节码行号

执行 Native 方法时为 undefined

三、Java 虚拟机栈

3.1 栈帧结构

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

3.2 局部变量表

public void method(int a, long b, String c) {
    int d = 10;  // 局部变量
}

Slot 分配

3.3 操作数栈

// i = a + b * c
iload_1      // 加载 a
iload_2      // 加载 b
iload_3      // 加载 c
imul         // b * c
iadd         // a + (b * c)
istore 4     // 存储到 i

3.4 常见异常

// StackOverflowError - 栈深度超限
public void recursive() {
    recursive(); // 无限递归
}

// OutOfMemoryError - 栈容量不足
// -Xss 设置过小,线程数过多

四、本地方法栈

4.1 作用

4.2 Native 方法示例

// System.currentTimeMillis() 是 Native 方法
public static native long currentTimeMillis();

// 底层调用操作系统 API

五、堆(Heap)

5.1 堆结构

堆内存
├── 新生代(Young Generation)
│   ├── Eden 区(8)
│   ├── Survivor0 区(1)
│   └── Survivor1 区(1)
└── 老年代(Old Generation)
    └── 大对象可直接进入老年代

比例:Eden : Survivor0 : Survivor1 = 8 : 1 : 1

5.2 对象分配流程

flowchart TD
    A[新对象] --> B{Eden 区<br>有空间?}
    B -->|是 | C[分配 Eden]
    B -->|否 | D[Minor GC]
    D --> E{Survivor<br>有空间?}
    E -->|是 | F[移动到 Survivor]
    E -->|否 | G[进入老年代]

5.3 对象内存布局

对象头(Header)
├── Mark Word(对象哈希、GC 信息、锁状态)
├── Klass Pointer(类型指针)
└── 数组长度(可选)

实例数据(Instance Data)
└── 继承的字段 + 自身字段

对齐填充(Padding)
└── 8 字节对齐

5.4 常见异常

# OutOfMemoryError: Java heap space
# 原因:对象过多,内存不足
# 解决:-Xmx 增大堆内存,排查内存泄漏

# OutOfMemoryError: GC overhead limit exceeded
# 原因:GC 时间超过 98%,回收内存不到 2%
# 解决:优化代码,减少对象创建

六、方法区(Method Area)

6.1 存储内容

方法区
├── 类信息(版本、字段、方法、接口)
├── 常量池(字符串常量、类引用)
├── 静态变量
├── 即时编译代码缓存
└── RunTimeConstantPool

6.2 运行时常量池

public class Test {
    // 编译期常量
    public static final int A = 1;
    
    // 运行时常量池
    String s1 = "abc";  // 字符串常量池
    String s2 = "abc";  // 同一引用
}

6.3 字符串常量池

String s1 = "abc";           // 常量池
String s2 = new String("abc"); // 堆中创建

System.out.println(s1 == s2);        // false
System.out.println(s1 == s2.intern()); // true

6.4 元空间(Metaspace)

JDK 8 变化


七、内存参数配置

7.1 常用参数

# 堆内存
-Xms256m          # 初始堆大小
-Xmx512m          # 最大堆大小
-Xmn128m          # 新生代大小

# 栈内存
-Xss256k          # 线程栈大小

# 方法区/元空间
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m

# GC 相关
-XX:+PrintGCDetails
-XX:+UseG1GC      # 使用 G1 收集器

7.2 内存查看

# 查看进程
jps -l

# 查看内存使用
jstat -gc <pid> 1000

# 内存 dump
jmap -dump:format=b,file=heap.hprof <pid>

# 分析工具
jhat heap.hprof  # JDK 自带
MAT              # Eclipse Memory Analyzer

八、内存泄漏排查

8.1 常见原因

  1. 静态集合:static Map/List 持有对象引用
  2. 未关闭资源:IO 流、数据库连接
  3. 监听器未注销:回调接口未移除
  4. ThreadLocal 未清理:线程复用导致

8.2 排查步骤

1. 监控告警 → 2. dump 内存 → 3. MAT 分析 → 4. 定位代码

8.3 示例

// ❌ 内存泄漏
static List<Object> cache = new ArrayList<>();
public void add(Object obj) {
    cache.add(obj); // 只增不减
}

// ✅ 修复
WeakReference<Object> ref = new WeakReference<>(obj);

九、总结

JVM 内存核心要点:

区域作用线程异常
PC 寄存器指令地址私有
虚拟机栈方法调用私有StackOverflowError
本地方法栈Native 方法私有StackOverflowError
对象实例共享OutOfMemoryError
方法区类元数据共享OutOfMemoryError

理解内存结构是性能优化和问题排查的基础,建议结合 jstat、MAT 等工具实践。


分享这篇文章到:

上一篇文章
Java 垃圾回收机制详解
下一篇文章
Java IO 演进之路