Skip to content

JVM 内存模型与区域划分

运行时数据区

JVM 在运行 Java 程序时,会将内存划分为多个区域,各有不同的用途和生命周期。

┌─────────────────────────────────────────────────────────┐
│                      JVM 运行时数据区                     │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  ┌─────────────────┐  ┌─────────────────┐              │
│  │   方法区         │  │      堆          │   ← 线程共享  │
│  │  (Method Area)  │  │    (Heap)       │              │
│  └─────────────────┘  └─────────────────┘              │
│                                                         │
│  ┌──────────┐ ┌──────────┐ ┌──────────┐                │
│  │  虚拟机栈  │ │ 本地方法栈 │ │ 程序计数器 │  ← 线程私有   │
│  │  (Stack) │ │(Native)  │ │   (PC)    │               │
│  └──────────┘ └──────────┘ └──────────┘                │
│                                                         │
└─────────────────────────────────────────────────────────┘

线程私有区域

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

  • 记录当前线程正在执行的字节码指令地址
  • 唯一不会发生 OutOfMemoryError 的区域
  • 如果执行的是 Native 方法,计数器值为空(Undefined)

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

每个方法调用会创建一个栈帧,包含:

┌─────────────────────────────┐
│          栈帧                │
├─────────────────────────────┤
│  局部变量表 (Local Variables) │  ← 存储方法参数和局部变量
├─────────────────────────────┤
│  操作数栈 (Operand Stack)    │  ← 存储计算过程中的中间结果
├─────────────────────────────┤
│  动态链接 (Dynamic Linking)  │  ← 运行时常量池的引用
├─────────────────────────────┤
│  方法返回地址                 │  ← 方法返回后继续执行的位置
└─────────────────────────────┘

可能的异常

  • StackOverflowError:栈深度超过限制(如递归过深)
  • OutOfMemoryError:无法申请足够的栈内存
java
/**
 * 栈溢出示例
 * @author yjhu
 */
public class StackOverflowDemo {
    private int depth = 0;
    
    public void recursion() {
        depth++;
        recursion();  // 无限递归
    }
    
    public static void main(String[] args) {
        StackOverflowDemo demo = new StackOverflowDemo();
        try {
            demo.recursion();
        } catch (StackOverflowError e) {
            System.out.println("栈深度: " + demo.depth);
            // 默认栈大小下,通常 10000+ 次调用就会溢出
        }
    }
}

3. 本地方法栈(Native Method Stack)

与虚拟机栈类似,但服务于 Native 方法(如 C/C++ 实现的方法)。

线程共享区域

1. 堆(Heap)

Java 内存管理的核心区域,几乎所有对象实例都在堆上分配。

┌─────────────────────────────────────────────────────────┐
│                         堆 (Heap)                        │
├──────────────────────────────┬──────────────────────────┤
│         年轻代 (Young Gen)    │      老年代 (Old Gen)     │
├────────┬────────┬────────────┤                          │
│  Eden  │   S0   │     S1     │                          │
│  区    │(From)  │   (To)     │                          │
│  80%   │  10%   │    10%     │                          │
└────────┴────────┴────────────┴──────────────────────────┘

对象分配流程

  1. 新对象优先在 Eden 区分配
  2. Eden 满了触发 Minor GC,存活对象移到 Survivor 区
  3. 对象在 Survivor 区每熬过一次 GC,年龄 +1
  4. 年龄达到阈值(默认 15)晋升到老年代
  5. 老年代满了触发 Major GC / Full GC

2. 方法区(Method Area)

存储类的元信息、常量、静态变量等。

JDK 版本变化

版本实现方式特点
JDK 7 及之前永久代(PermGen)使用堆内存,容易 OOM
JDK 8 及之后元空间(Metaspace)使用本地内存,默认不限大小
bash
# JDK 7 设置永久代大小
-XX:PermSize=256m -XX:MaxPermSize=512m

# JDK 8+ 设置元空间大小
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m

直接内存(Direct Memory)

不属于 JVM 运行时数据区,但使用 NIO 时会用到。

java
// 分配直接内存
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);

优点:减少数据在 JVM 堆和系统内存之间的复制 缺点:分配和回收成本较高

内存参数设置

bash
# 堆内存
-Xms512m          # 初始堆大小
-Xmx2g            # 最大堆大小
-Xmn256m          # 年轻代大小

# 栈内存
-Xss256k          # 每个线程的栈大小

# 元空间
-XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize=256m

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

常见 OOM 场景

区域异常信息常见原因
Java heap space对象过多、内存泄漏
元空间Metaspace动态生成大量类(如 CGLib)
StackOverflowError递归过深
直接内存Direct buffer memoryNIO 使用不当