# 内存模型与内存区域

## 核心概念

JVM 运行时数据区是 Java 程序运行时内存的抽象模型，理解各区域的作用和特点是学习 JVM 的基础。

## 运行时数据区

### 1. 程序计数器（Program Counter Register）

* **线程私有**
* 记录当前线程执行的字节码指令地址
* 如果执行 Native 方法，计数器值为 Undefined
* **唯一不会发生 OOM 的区域**

### 2. Java 虚拟机栈（JVM Stack）

* **线程私有**，生命周期与线程相同
* 每个方法执行时创建一个**栈帧（Stack Frame）**

#### 栈帧结构

```
┌─────────────────────────┐
│        栈帧              │
├─────────────────────────┤
│  局部变量表              │ ← 存放方法参数和局部变量
│  (Local Variable Table) │    基本类型直接存值
│                         │    引用类型存指针
├─────────────────────────┤
│  操作数栈                │ ← 字节码指令的操作空间
│  (Operand Stack)        │    类似 CPU 的寄存器
├─────────────────────────┤
│  动态链接                │ ← 指向运行时常量池的
│  (Dynamic Linking)      │    方法引用
├─────────────────────────┤
│  方法返回地址             │ ← 方法正常/异常退出
│  (Return Address)       │    返回的位置
└─────────────────────────┘
```

#### 异常情况

* **StackOverflowError**：栈深度超过限制（递归调用过深）
* **OutOfMemoryError**：无法申请到足够内存（动态扩展时）

```java
// 触发 StackOverflowError
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);
            // 默认栈大小（-Xss）下大约几千次
        }
    }
}
```

### 3. 本地方法栈（Native Method Stack）

* **线程私有**
* 为 Native 方法服务
* HotSpot 将虚拟机栈和本地方法栈合二为一

### 4. Java 堆（Heap）⭐⭐⭐⭐⭐

* **线程共享**，JVM 启动时创建
* **GC 的主要管理区域**
* 几乎所有对象实例都在这里分配

#### 堆内存结构（分代）

```
┌──────────────────────────────────────────────┐
│                   Java 堆                     │
├────────────────────────┬─────────────────────┤
│       新生代 (Young)    │    老年代 (Old)      │
│     默认占 1/3 堆       │   默认占 2/3 堆      │
├──────┬──────┬──────────┤                     │
│ Eden │  S0  │    S1    │                     │
│ 8/10 │ 1/10 │   1/10  │                     │
└──────┴──────┴──────────┴─────────────────────┘
       默认比例 Eden:S0:S1 = 8:1:1
```

#### JVM 参数

```bash
-Xms        # 堆初始大小
-Xmx        # 堆最大大小
-Xmn        # 新生代大小
-XX:NewRatio=2              # 老年代:新生代 = 2:1
-XX:SurvivorRatio=8         # Eden:S0:S1 = 8:1:1
-XX:MaxTenuringThreshold=15 # 晋升老年代的年龄阈值
```

#### 堆内存 OOM

```java
// 触发 java.lang.OutOfMemoryError: Java heap space
// JVM 参数: -Xms20m -Xmx20m
public class HeapOomDemo {
    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();
        while (true) {
            list.add(new byte[1024 * 1024]); // 每次分配 1MB
        }
    }
}
```

### 5. 方法区（Method Area）

* **线程共享**
* 存储类信息、常量、静态变量、JIT 编译后的代码

#### 实现演变

| JDK 版本    | 实现方式           | 存储位置                |
| --------- | -------------- | ------------------- |
| JDK 7 及之前 | 永久代（PermGen）   | JVM 内存              |
| JDK 8 及之后 | 元空间（Metaspace） | 本地内存（Native Memory） |

#### 为什么用元空间替换永久代

1. **永久代大小难以确定**：容易出现 OOM
2. **GC 效率低**：永久代的回收条件苛刻
3. **与 JRockit 合并**：JRockit 没有永久代的概念
4. **灵活性更高**：元空间使用本地内存，不受 JVM 堆大小限制

#### 元空间参数

```bash
-XX:MetaspaceSize=256m       # 元空间初始大小
-XX:MaxMetaspaceSize=512m    # 元空间最大大小（默认无限制）
```

### 6. 运行时常量池（Runtime Constant Pool）

* 方法区的一部分
* Class 文件中的常量池表在类加载后进入运行时常量池
* 具有**动态性**：运行时也可以添加常量（如 `String.intern()`）

### 7. 直接内存（Direct Memory）

* 不属于 JVM 运行时数据区
* NIO 使用 `DirectByteBuffer` 直接操作堆外内存
* 通过 `-XX:MaxDirectMemorySize` 控制大小
* 避免在 Java 堆和 Native 堆之间复制数据

## 对象的创建过程

```
         ┌─────────────┐
         │  new 指令    │
         └──────┬──────┘
                ▼
         ┌─────────────┐
    ┌────│ 类加载检查    │ ← 检查类是否已加载、解析、初始化
    │    └──────┬──────┘
    │           ▼
    │    ┌─────────────┐     ┌──────────────────────┐
    │    │  内存分配     │────▶│ 指针碰撞（内存规整时）  │
    │    └──────┬──────┘     │ 空闲列表（内存不规整时）│
    │           │            │ TLAB（线程安全分配）    │
    │           ▼            └──────────────────────┘
    │    ┌─────────────┐
    │    │ 初始化零值    │ ← 保证字段不赋初值就能使用
    │    └──────┬──────┘
    │           ▼
    │    ┌─────────────┐
    │    │ 设置对象头    │ ← 类元数据指针、哈希码、GC 分代年龄、锁标志
    │    └──────┬──────┘
    │           ▼
    │    ┌─────────────┐
    └───▶│ 执行 <init>  │ ← 构造方法，真正的初始化
         └─────────────┘
```

## 对象的内存布局

### 对象头（Object Header）

在 64 位 JVM 中：

```
┌────────────────────────────────────────────────────┐
│                  对象头（96 bit / 128 bit）           │
├────────────────────────────────────┬───────────────┤
│        Mark Word (64 bit)         │ Klass Pointer │
│                                    │   (32/64 bit) │
├────────────────────────────────────┴───────────────┤
│  锁标志位 │ 分代年龄 │ HashCode │ 线程 ID │ 锁指针    │
└────────────────────────────────────────────────────┘

Mark Word 在不同锁状态下的内容：
┌───────────────────────────────┬──────────┬─────────┐
│         Mark Word 内容         │ 锁标志位  │  状态   │
├───────────────────────────────┼──────────┼─────────┤
│ HashCode | Age | 0            │   01     │ 无锁    │
│ Thread ID | Epoch | Age | 1   │   01     │ 偏向锁  │
│ 指向栈中锁记录的指针            │   00     │ 轻量级锁 │
│ 指向 Monitor 的指针            │   10     │ 重量级锁 │
│ 空                             │   11     │ GC 标记 │
└───────────────────────────────┴──────────┴─────────┘
```

### 实例数据（Instance Data）

* 对象真正存储的有效信息
* 字段的存储顺序受分配策略和定义顺序影响
* 相同宽度的字段总是被分配在一起

### 对齐填充（Padding）

* HotSpot 要求对象起始地址必须是 **8 字节的整数倍**
* 不够则填充

## 对象的访问定位

### 句柄访问

```
栈 → 句柄池 → 对象实例数据（堆中）
              → 对象类型数据（方法区中）
```

* 优点：对象移动时只需修改句柄中的指针
* 缺点：两次指针定位

### 直接指针（HotSpot 使用）

```
栈 → 对象实例数据（堆中）→ 对象类型数据（方法区中）
```

* 优点：速度快，一次指针定位
* 缺点：对象移动时需要修改栈中引用

## 面试要点

### 高频问题

1. **JVM 运行时数据区有哪些？各自的作用？**
   * 程序计数器、虚拟机栈、本地方法栈（线程私有）
   * 堆、方法区（线程共享）
2. **堆和栈的区别？**
   * 栈：线程私有，存储局部变量和方法调用，自动分配和回收
   * 堆：线程共享，存储对象实例，由 GC 管理
3. **什么是 TLAB？为什么需要它？**
   * Thread Local Allocation Buffer，线程本地分配缓冲区
   * 每个线程在堆的 Eden 区预先分配一小块内存
   * 避免多线程分配内存时的锁竞争
4. **永久代和元空间的区别？**
   * 永久代使用 JVM 内存，元空间使用本地内存
   * 元空间默认无上限，更不容易 OOM
   * JDK 8 起永久代被移除
5. **String.intern() 在 JDK 6 和 JDK 7+ 的区别？**
   * JDK 6：将字符串复制到永久代的字符串常量池
   * JDK 7+：字符串常量池移到堆中，intern() 只存引用

### 常见陷阱

* 不是所有对象都在堆上分配（逃逸分析 → 栈上分配）
* 方法区不等于永久代，永久代只是 HotSpot 对方法区的实现
* `-Xss` 设置的是每个线程的栈大小，不是所有线程共享

## 参考资料

* 《深入理解 Java 虚拟机》第 2 章 - 周志明
* [JVM Specification: Run-Time Data Areas](https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-2.html#jvms-2.5)
* [JEP 122: Remove the Permanent Generation](https://openjdk.org/jeps/122)
