JVM 内存结构全链路:堆、栈、方法区、元空间

JVM 内存结构全链路:堆、栈、方法区、元空间

面试必问的 JVM 内存模型,从堆栈到元空间,每个区域的作用、OOM 类型和调优方向一次性讲清楚。

一、JVM 内存结构全景

scss 复制代码
┌─────────────────────────────────────────────────────────────┐
│                     JVM 内存结构全景                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌───────────────────────────────────────────────────────┐  │
│  │                   线程共享区域                          │  │
│  │  ┌──────────────────────────────────────────────┐     │  │
│  │  │              堆 (Heap)                         │     │  │
│  │  │  ┌──────────────┬──────────────────────────┐  │     │  │
│  │  │  │  新生代       │     老年代               │  │     │  │
│  │  │  │ (Young Gen)  │   (Old Gen)              │  │     │  │
│  │  │  │ ┌───┬──┬───┐│                          │  │     │  │
│  │  │  │ │Eden│S0│S1││                          │  │     │  │
│  │  │  │ └───┴──┴───┘│                          │  │     │  │
│  │  │  └──────────────┴──────────────────────────┘  │     │  │
│  │  └──────────────────────────────────────────────┘     │  │
│  │  ┌──────────────────────────────────────────────┐     │  │
│  │  │         方法区 / 元空间 (Metaspace)            │     │  │
│  │  │  类元数据、常量池、静态变量、方法字节码          │     │  │
│  │  └──────────────────────────────────────────────┘     │  │
│  └───────────────────────────────────────────────────────┘  │
│                                                             │
│  ┌───────────────────────────────────────────────────────┐  │
│  │                   线程私有区域                        │  │
│  │  ┌──────────┐  ┌──────────┐  ┌──────────────────┐    │  │
│  │  │  虚拟机栈 │  │ 本地方法栈 │  │  程序计数器(PC)    │    │  │
│  │  │ (VM Stack)│  │(Native    │  │ (Program Counter  │    │  │
│  │  │          │  │ Stack)    │  │  Register)        │    │  │
│  │  └──────────┘  └──────────┘  └──────────────────┘    │  │
│  └───────────────────────────────────────────────────────┘  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

二、各区域详解

1. 程序计数器(PC Register)

java 复制代码
// 每个线程独立拥有,记录当前执行的字节码行号
// 不会 OOM,是唯一不会 OOM 的区域
特性 说明
线程私有 每个线程独立
作用 记录当前线程执行到的字节码行号
OOM 不可能发生
大小 很小(一个 word)

2. 虚拟机栈(VM Stack)

java 复制代码
public class StackDemo {
    public void methodA() {
        int a = 1;           // 局部变量
        Object obj = new Object(); // 引用
        methodB();            // 方法调用 → 压入新栈帧
    }

    public void methodB() {
        int b = 2;
        // 栈帧结构:
        // ┌─────────────────────┐
        // │  局部变量表           │ a, b, obj, this...
        // ├─────────────────────┤
        // │  操作数栈             │ 计算中间结果
        // ├─────────────────────┤
        // │  动态链接             │ 指向运行时常量池
        // ├─────────────────────┤
        // │  方法返回地址         │ 调用者PC
        // └─────────────────────┘
    }
}
ini 复制代码
虚拟机栈结构:

  线程1的虚拟机栈
  ┌─────────────────┐
  │  methodA 栈帧    │ ← 栈顶(当前执行)
  │  ┌─────────────┐│
  │  │局部变量表     ││ this, a=1, obj
  │  │操作数栈      ││
  │  │动态链接      ││
  │  │返回地址      ││
  │  └─────────────┘│
  ├─────────────────┤
  │  main 栈帧       │
  │  ┌─────────────┐│
  │  │局部变量表     ││ args
  │  │操作数栈      ││
  │  │动态链接      ││
  │  │返回地址      ││
  │  └─────────────┘│
  └─────────────────┘
异常类型 触发条件
StackOverflowError 栈深度超过限制(递归过深)
OutOfMemoryError 栈扩展时无法申请到足够内存

3. 本地方法栈

与虚拟机栈类似,区别在于服务对象是 Native 方法。HotSpot 中与虚拟机栈合二为一。

4. 堆(Heap)

堆是 JVM 管理的最大内存区域,存放对象实例和数组:

java 复制代码
// 所有 new 出来的对象都在堆中
Object obj = new Object();  // obj 引用在栈中,Object 对象在堆中

int[] arr = new int[100];   // 数组也在堆中
makefile 复制代码
堆内存分代结构:

┌──────────────────────────────────────────────────────┐
│                    堆 (Heap)                          │
├───────────────────────┬──────────────────────────────┤
│     新生代 (1/3)      │      老年代 (2/3)             │
│  ┌─────┬────┬────┐   │                              │
│  │Eden │ S0 │ S1 │   │   长期存活的对象               │
│  │ 8   │ 1  │ 1  │   │   大对象直接进入               │
│  └─────┴────┴────┘   │   Major GC / Full GC          │
│  Minor GC            │                              │
│  复制算法             │   标记-清除/标记-整理           │
└───────────────────────┴──────────────────────────────┘

对象分配流程:
  new 对象 → Eden 区分配
    Eden 满 → Minor GC
      存活 → 复制到 S0/S1,年龄+1
        年龄 ≥ 15 → 晋升老年代
          大对象 → 直接进老年代
异常 触发条件
OutOfMemoryError: Java heap space 堆内存不足,无法分配新对象
OutOfMemoryError: GC overhead limit exceeded GC 花费超过 98% 时间但回收不到 2% 内存

5. 方法区 / 元空间

java 复制代码
方法区的演进:
  JDK 7 之前: 永久代(Permanent Generation) --- 堆的一部分,JVM 管理
  JDK 7:     符号引用移到 native memory, 静态变量移到堆
  JDK 8+:    元空间(Metaspace) --- 使用本地内存(Native Memory)
java 复制代码
// 方法区存储的内容
public class MethodAreaDemo {
    // 静态变量 → JDK 7+ 移到堆
    private static final int MAX = 100;

    // 类信息、方法字节码 → 元空间
    public void process() {
        // 方法字节码存储在元空间
    }

    // 常量池中的字符串
    String s1 = "abc";   // 常量池
    String s2 = new String("abc"); // 堆中对象
}
对比项 永久代(JDK 7-) 元空间(JDK 8+)
位置 JVM 堆中 本地内存
大小限制 -XX:PermSize / -XX:MaxPermSize -XX:MetaspaceSize / -XX:MaxMetaspaceSize
GC Full GC 时回收 达到阈值触发 Full GC
OOM java.lang.OutOfMemoryError: PermGen space java.lang.OutOfMemoryError: Metaspace
动态性 容易 OOM(固定大小) 可用本地内存,不易 OOM

为什么要用元空间替代永久代?

  1. 永久代大小固定,容易在大量动态生成类的场景下 OOM
  2. 元空间使用本地内存,大小受限于物理内存,更灵活
  3. 方便与 JRockit 合并(JRockit 没有永久代)

三、对象在堆中的分配策略

java 复制代码
public class AllocationDemo {
    public static void main(String[] args) {
        // 小对象 → Eden 区
        Object small = new Object();

        // 大对象 → 老年代(避免 Eden→Survivor 复制开销)
        // -XX:PretenureSizeThreshold=1M
        byte[] large = new byte[2 * 1024 * 1024]; // 2MB → 直接进老年代

        // 长期存活对象 → 老年代
        // 对象头有 GC 年龄,每次 Minor GC 存活则 +1
        // -XX:MaxTenuringThreshold=15 (默认)
        Object longLived = new Object();
        // 经过 15 次 Minor GC 仍存活 → 晋升老年代
    }
}
arduino 复制代码
对象分配决策树:

  new 对象
     │
     ▼
  是否大对象? ──── 是 ───▶ 直接进老年代
     │ 否
     ▼
  尝试在 TLAB 分配 ──── 成功 ───▶ 分配完成
     │ 失败
     ▼
  尝试在 Eden 区 CAS 分配 ──── 成功 ───▶ 分配完成
     │ 失败
     ▼
  Minor GC
     │
     ▼
  存活对象年龄+1
     │
     ├─ 年龄 ≥ 阈值 ─────────▶ 晋升老年代
     ├─ Survivor 区放不下 ────▶ 晋升老年代
     └─ 否 ──────────────────▶ 复制到另一 Survivor

TLAB(Thread Local Allocation Buffer)

java 复制代码
// 每个线程在 Eden 区有一小块私有区域
// 避免多线程分配时的 CAS 竞争
// -XX:+UseTLAB (默认开启)

// 分配流程:
// 1. 先在 TLAB 中分配(无锁)
// 2. TLAB 用完 → CAS 在 Eden 区分配
// 3. Eden 满 → Minor GC

四、对象访问定位

java 复制代码
Object obj = new Object();
// 栈中的 reference → 堆中的 Object 对象
markdown 复制代码
访问方式: 句柄 vs 直接指针

句柄方式 (JDK 早期):
  栈 (reference) ──▶ 堆 (句柄池)
                        │
                        ├─▶ 对象实例数据 (堆)
                        └─▶ 对象类型数据 (元空间)

直接指针方式 (HotSpot):
  栈 (reference) ──▶ 堆 (对象头 + 实例数据)
                        │
                        └─▶ 对象类型数据 (元空间)
对比项 句柄 直接指针
优点 对象移动时只改句柄指针 访问少一次间接跳转,速度快
缺点 多一次访问 对象移动时需修改所有引用
使用 无 HotSpot 使用

五、各区域 OOM 类型总结

java 复制代码
// 1. 堆 OOM
List<byte[]> list = new ArrayList<>();
while (true) {
    list.add(new byte[1024 * 1024]); // 不断创建 1MB 对象
}
// Exception: java.lang.OutOfMemoryError: Java heap space

// 2. 栈溢出
public void recursive() {
    recursive(); // 无限递归
}
// Exception: java.lang.StackOverflowError

// 3. 元空间 OOM
// 大量动态生成类(如 CGLIB 代理类)
// Exception: java.lang.OutOfMemoryError: Metaspace

// 4. 直接内存 OOM
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024 * 1024);
// Exception: java.lang.OutOfMemoryError: Direct buffer memory
OOM 类型 原因 解决方向
Java heap space 堆内存不足 增大 -Xmx,检查内存泄漏
GC overhead limit GC 回收效率过低 增大堆或排查泄漏
StackOverflowError 栈深度超限 检查递归,增大 -Xss
Metaspace 元空间不足 增大 -XX:MaxMetaspaceSize
Direct buffer memory 直接内存不足 检查 NIO Buffer 使用

六、常用 JVM 参数

参数 作用 示例
-Xms 初始堆大小 -Xms256m
-Xmx 最大堆大小 -Xmx1024m
-Xmn 新生代大小 -Xmn256m
-Xss 栈大小 -Xss512k
-XX:MetaspaceSize 元空间初始大小 -XX:MetaspaceSize=128m
-XX:MaxMetaspaceSize 元空间最大大小 -XX:MaxMetaspaceSize=256m
-XX:SurvivorRatio Eden:Survivor 比例 -XX:SurvivorRatio=8
-XX:MaxTenuringThreshold 晋升老年代年龄 -XX:MaxTenuringThreshold=15
-XX:PretenureSizeThreshold 大对象阈值 -XX:PretenureSizeThreshold=1M
-XX:+UseTLAB 启用 TLAB 默认开启

七、实战:内存泄漏排查

java 复制代码
// 常见内存泄漏场景
public class MemoryLeakDemo {
    // 1. 静态集合持有对象引用
    private static final Map<String, Object> cache = new HashMap<>();
    public void addToCache(String key, Object value) {
        cache.put(key, value); // 永远不清理 → 泄漏
    }

    // 2. 未关闭的资源
    public void readFile() throws IOException {
        InputStream is = new FileInputStream("file.txt");
        // 忘记 is.close() → 泄漏
    }

    // 3. 监听器未移除
    public void addListener() {
        button.addActionListener(e -> {
            // 匿名类隐式持有外部类引用
            // 如果 button 生命周期长 → 外部类无法回收
        });
    }

    // 4. ThreadLocal 未 remove
    private ThreadLocal<Object> threadLocal = new ThreadLocal<>();
    public void process() {
        threadLocal.set(new Object());
        // 忘记 threadLocal.remove() → 线程池场景泄漏
    }
}
markdown 复制代码
排查步骤:
  1. jps → 找到进程 PID
  2. jstat -gcutil <pid> 1000 → 观察 GC 情况
  3. jmap -heap <pid> → 查看堆内存分布
  4. jmap -dump:format=b,file=heap.hprof <pid> → 导出堆转储
  5. MAT / VisualVM 分析 hprof 文件
  6. 找到 GC Root 引用链,定位泄漏对象

八、面试高频追问

Q1:为什么要把永久代替换为元空间?

永久代大小固定容易 OOM,尤其是动态代理、动态语言等场景大量生成类。元空间使用本地内存,大小灵活,不易 OOM。

Q2:对象一定分配在堆上吗?

不一定。JIT 的逃逸分析可以证明对象不逃逸时,通过标量替换在栈上分配,避免堆分配和 GC 压力。

Q3:为什么 Eden:S0:S1 = 8:1:1?

新生代中大部分对象朝生夕死,98% 的对象在 Minor GC 时被回收。复制算法只需要 10% 的空间(S0 或 S1)作为备用,Eden 区有 80% 空间可用,整体利用率 90%,远高于 50%(两个 Survivor 各占一半的情况)。

Q4:栈上分配和 TLAB 是一回事吗?

不是。栈上分配是 JIT 逃逸分析后的优化,对象分配在栈帧中,随方法退出自动销毁。TLAB 是堆中 Eden 区的线程私有区域,减少多线程分配时的 CAS 竞争,对象仍在堆中。

九、总结

css 复制代码
JVM 内存结构:
  线程共享: 堆(对象/数组) + 元空间(类元数据/常量池)
  线程私有: 虚拟机栈(栈帧) + 本地方法栈 + 程序计数器

  堆:  新生代(Eden:S0:S1=8:1:1) + 老年代
       Minor GC(复制) + Full GC(标记-整理)
  栈:  StackOverflowError(递归过深) / OOM(无法扩展)
  元空间: 本地内存, -XX:MaxMetaspaceSize 限制
  
  对象分配: TLAB → Eden → Survivor → 老年代
  大对象: 直接进老年代
  GC Roots: 栈引用、静态变量、常量、JNI 引用

理解 JVM 内存结构是性能调优和排查 OOM 问题的基础,面试中每个区域的作用、OOM 类型和调优参数都是高频考点。

相关推荐
IT枫斗者枫哥1 小时前
Spring Boot导入返回409,为什么第一行还是入库了?
java·spring boot·后端
xiaoqiMikko1 小时前
JVM 线上排查实战(五):升 JDK 17 后进程起不来,十几个老 GC 参数挨个实测(完结)
java·jvm
Flynt1 小时前
Java 27 悄悄改了 3 个默认值,我在 1 核小机器上逐个验证了一遍
java·jvm·性能优化
天天被压力1 小时前
【Python 量化取数指南 #09】Python 拿港股通数据:港股通成交与港股财报实测
java·人工智能·python
吃饱了得干活1 小时前
数据放哪儿:从 HashMap 到一致性哈希
java·后端
SL_staff1 小时前
四维穿透式齐套判断:JVS-APS在制造计划系统中的技术实现
java·spring boot·python
杨杨杨大侠1 小时前
多 Agent 协作到底靠什么?从 Codex 内部机制到 A2A 企业服务
java·人工智能·agent
AI深栈1 小时前
第 17 章 · 编排 AI 流程:StateGraph 节点、条件边与意图路由
java·人工智能
吃饱了得干活1 小时前
Hash 全景:从 HashMap 到一致性哈希,一文吃透哈希核心
java·后端