面试必问的 JVM 内存模型,一张图画清楚

一、引言

"请谈谈 JVM 的内存模型?"

这是 Java 面试中最常见的问题之一,也是考察候选人基础是否扎实的关键问题。很多人能说出堆、栈、方法区这些名词,但要讲清楚每个区域的作用、特点和 OOM 场景,就需要对 JVM 内存模型有深入的理解。

今天,我用一张全景图带你彻底搞懂 JVM 内存模型。


二、JVM 内存模型全景图

┌──────────────────────────────────────────────────────────────────────────┐
│                         JVM 运行时数据区域                                │
│                                                                          │
│  ┌─────────────────────────────────────────────────────────────────────┐ │
│  │                         线程私有区域                                  │ │
│  │                                                                     │ │
│  │  ┌───────────────────────────────────────────────────────────────┐  │ │
│  │  │                  程序计数器 (Program Counter)                  │  │ │
│  │  │  ┌─────────────────────────────────────────────────────────┐  │  │ │
│  │  │  │  作用: 记录当前线程执行的字节码行号                        │  │  │ │
│  │  │  │  特点: 唯一不会 OOM 的区域, 线程私有                     │  │  │ │
│  │  │  │  存储: 当前执行指令地址、native 方法返回值               │  │  │ │
│  │  │  └─────────────────────────────────────────────────────────┘  │  │ │
│  │  └───────────────────────────────────────────────────────────────┘  │ │
│  │                                                                     │ │
│  │  ┌───────────────────────────────────────────────────────────────┐  │ │
│  │  │               虚拟机栈 (Java Virtual Machine Stack)           │  │ │
│  │  │                                                               │  │ │
│  │  │  ┌─────────────────────────────────────────────────────────┐  │  │ │
│  │  │  │                    栈帧 (Stack Frame)                    │  │  │ │
│  │  │  │  ┌───────────────────────────────────────────────────┐  │  │  │ │
│  │  │  │  │    局部变量表 (Local Variable Table)              │  │  │  │ │
│  │  │  │  │    ┌───────────────────────────────────────────┐  │  │  │  │ │
│  │  │  │  │    │ this, 参数, 局部变量, 64位类型占2槽位      │  │  │  │  │ │
│  │  │  │  │    └───────────────────────────────────────────┘  │  │  │  │ │
│  │  │  │  ├───────────────────────────────────────────────────┤  │  │  │ │
│  │  │  │  │    操作数栈 (Operand Stack)                      │  │  │  │ │
│  │  │  │  │    ┌───────────────────────────────────────────┐  │  │  │  │ │
│  │  │  │  │    │ 临时操作数存储, 方法调用参数传递            │  │  │  │  │ │
│  │  │  │  │    └───────────────────────────────────────────┘  │  │  │  │ │
│  │  │  │  ├───────────────────────────────────────────────────┤  │  │  │ │
│  │  │  │  │    动态链接 (Dynamic Linking)                    │  │  │  │ │
│  │  │  │  │    ┌───────────────────────────────────────────┐  │  │  │  │ │
│  │  │  │  │    │ 指向运行时常量池中方法的符号引用            │  │  │  │  │ │
│  │  │  │  │    └───────────────────────────────────────────┘  │  │  │  │ │
│  │  │  │  ├───────────────────────────────────────────────────┤  │  │  │ │
│  │  │  │  │    方法返回地址 (Return Address)                 │  │  │  │ │
│  │  │  │  │    ┌───────────────────────────────────────────┐  │  │  │  │ │
│  │  │  │  │    │ 方法调用前的PC值, 用于异常或正常返回        │  │  │  │  │ │
│  │  │  │  │    └───────────────────────────────────────────┘  │  │  │  │ │
│  │  │  │  └───────────────────────────────────────────────────┘  │  │  │ │
│  │  │  └─────────────────────────────────────────────────────────┘  │  │ │
│  │  │                                                               │  │ │
│  │  │  特点: 线程私有, LIFO, 栈深度固定                              │  │ │
│  │  │  OOM: StackOverflowError(递归太深), OutOfMemoryError(栈太多)   │  │ │
│  │  │  参数: -Xss (栈大小, 默认1MB)                                  │  │ │
│  │  └───────────────────────────────────────────────────────────────┘  │ │
│  │                                                                     │ │
│  │  ┌───────────────────────────────────────────────────────────────┐  │ │
│  │  │               本地方法栈 (Native Method Stack)               │  │ │
│  │  │                                                               │  │ │
│  │  │  作用: 为 native 方法(如JNI)提供栈空间                        │  │ │
│  │  │  特点: 与虚拟机栈类似, 但服务于 native 方法                   │  │ │
│  │  │  OOM: StackOverflowError, OutOfMemoryError                   │  │ │
│  │  │  参数: -Xoss (本地方法栈大小)                                 │  │ │
│  │  └───────────────────────────────────────────────────────────────┘  │ │
│  └─────────────────────────────────────────────────────────────────────┘ │
│                                                                          │
│  ┌─────────────────────────────────────────────────────────────────────┐ │
│  │                         线程共享区域                                  │ │
│  │                                                                     │ │
│  │  ┌───────────────────────────────────────────────────────────────┐  │ │
│  │  │                          堆 (Heap)                            │  │ │
│  │  │                                                               │  │ │
│  │  │  ┌─────────────────────────────────────────────────────────┐  │  │ │
│  │  │  │                    新生代 (Young Generation)             │  │  │ │
│  │  │  │  ┌───────────────────────────────────────────────────┐  │  │  │ │
│  │  │  │  │                    Eden Space                     │  │  │  │ │
│  │  │  │  │  作用: 新对象分配区                               │  │  │  │ │
│  │  │  │  │  容量: 约占新生代的80%                            │  │  │  │ │
│  │  │  │  │  GC: Minor GC 时存活对象进入 Survivor              │  │  │  │ │
│  │  │  │  └───────────────────────────────────────────────────┘  │  │  │ │
│  │  │  │  ┌───────────────────────────────────────────────────┐  │  │  │ │
│  │  │  │  │              Survivor Space 0 (S0/From)           │  │  │  │ │
│  │  │  │  │  作用: 存放 Minor GC 后存活的对象                 │  │  │  │ │
│  │  │  │  │  容量: 约占新生代的10%                            │  │  │  │ │
│  │  │  │  │  GC: 与 S1 互相复制, 年龄达到阈值进入老年代       │  │  │  │ │
│  │  │  │  └───────────────────────────────────────────────────┘  │  │  │ │
│  │  │  │  ┌───────────────────────────────────────────────────┐  │  │  │ │
│  │  │  │  │              Survivor Space 1 (S1/To)             │  │  │  │ │
│  │  │  │  │  作用: 与 S0 交替使用                            │  │  │  │ │
│  │  │  │  │  容量: 约占新生代的10%                            │  │  │  │ │
│  │  │  │  │  GC: 始终保持为空, 作为复制目标                   │  │  │  │ │
│  │  │  │  └───────────────────────────────────────────────────┘  │  │  │ │
│  │  │  │                                                         │  │  │ │
│  │  │  │  新生代特点:                                              │  │  │ │
│  │  │  │  ├── 对象生命周期短, 98% 对象很快被回收                   │  │  │ │
│  │  │  │  ├── 使用复制算法 (Copying)                             │  │  │ │
│  │  │  │  ├── Minor GC 频率高、速度快                            │  │  │ │
│  │  │  │  └── 对象年龄阈值: 默认15 (-XX:MaxTenuringThreshold)    │  │  │ │
│  │  │  └─────────────────────────────────────────────────────────┘  │  │ │
│  │  │                                                               │  │ │
│  │  │  ┌─────────────────────────────────────────────────────────┐  │  │ │
│  │  │  │                   老年代 (Old Generation)               │  │  │ │
│  │  │  │                                                         │  │  │ │
│  │  │  │  作用: 存放存活时间长的对象                              │  │  │ │
│  │  │  │  容量: 约占堆的2/3                                      │  │  │ │
│  │  │  │  GC: Major GC/Full GC, 使用标记-清除或标记-整理算法       │  │  │ │
│  │  │  │                                                         │  │  │ │
│  │  │  │  老年代特点:                                              │  │  │ │
│  │  │  │  ├── 对象生命周期长, 存活对象多                           │  │  │ │
│  │  │  │  ├── 使用标记-清除/标记-整理算法                         │  │  │ │
│  │  │  │  ├── Major GC 频率低、速度慢                            │  │  │ │
│  │  │  │  └── 大对象直接进入老年代 (-XX:PretenureSizeThreshold)   │  │  │ │
│  │  │  └─────────────────────────────────────────────────────────┘  │  │ │
│  │  │                                                               │  │ │
│  │  │  堆特点:                                                       │  │ │
│  │  │  ├── 所有线程共享, 最大的一块内存区域                         │  │ │
│  │  │  ├── 存放对象实例和数组                                       │  │ │
│  │  │  ├── GC 的主要战场                                            │  │ │
│  │  │  └── OOM: java.lang.OutOfMemoryError: Java heap space        │  │ │
│  │  │                                                               │  │ │
│  │  │  参数:                                                         │  │ │
│  │  │  ├── -Xms: 堆最小容量 (默认物理内存1/64)                      │  │ │
│  │  │  ├── -Xmx: 堆最大容量 (默认物理内存1/4)                      │  │ │
│  │  │  └── -XX:NewRatio: 新生代/老年代比例 (默认2)                  │  │ │
│  │  └───────────────────────────────────────────────────────────────┘  │ │
│  │                                                                     │ │
│  │  ┌───────────────────────────────────────────────────────────────┐  │ │
│  │  │                      方法区 (Method Area)                    │  │ │
│  │  │                                                               │  │ │
│  │  │  ┌─────────────────────────────────────────────────────────┐  │  │ │
│  │  │  │               运行时常量池 (Runtime Constant Pool)       │  │  │ │
│  │  │  │                                                         │  │  │ │
│  │  │  │  内容:                                                     │  │  │ │
│  │  │  │  ├── 字面量 (字符串、final常量)                           │  │  │ │
│  │  │  │  ├── 符号引用 (类、方法、字段的全限定名)                   │  │  │ │
│  │  │  │  └── 直接引用 (解析后的内存地址)                           │  │  │ │
│  │  │  │                                                         │  │  │ │
│  │  │  │  特点:                                                     │  │  │ │
│  │  │  │  ├── Class 文件常量池在类加载时进入                         │  │  │ │
│  │  │  │  ├── 动态生成的常量 (如String.intern())                   │  │  │ │
│  │  │  │  └── JDK 7+ 字符串常量移至堆中                            │  │  │ │
│  │  │  └─────────────────────────────────────────────────────────┘  │  │ │
│  │  │                                                               │  │ │
│  │  │  ┌─────────────────────────────────────────────────────────┐  │  │ │
│  │  │  │                      元空间 (Metaspace)                  │  │  │ │
│  │  │  │                      (JDK 8+)                           │  │  │ │
│  │  │  │                                                         │  │  │ │
│  │  │  │  内容:                                                     │  │  │ │
│  │  │  │  ├── 类信息 (名称、访问修饰符)                            │  │  │ │
│  │  │  │  ├── 字段信息 (名称、类型、修饰符)                         │  │  │ │
│  │  │  │  ├── 方法信息 (名称、参数、返回值、字节码)                  │  │  │ │
│  │  │  │  ├── 常量池 (运行时常量池)                                │  │  │ │
│  │  │  │  └── 静态变量                                             │  │  │ │
│  │  │  │                                                         │  │  │ │
│  │  │  │  特点 (JDK 8+ Metaspace vs JDK 7 PermGen):                │  │  │ │
│  │  │  │                                                         │  │  │ │
│  │  │  │  ┌───────────────────────────────────────────────────┐  │  │  │ │
│  │  │  │  │  对比维度      PermGen (JDK 7)    Metaspace (JDK 8+) │  │  │  │ │
│  │  │  │  │  ───────────   ──────────────    ────────────────── │  │  │  │ │
│  │  │  │  │  内存位置      堆内存内            本地内存 (Native)  │  │  │  │ │
│  │  │  │  │  大小限制      固定大小 (-XX:MaxPermSize)            │  │  │  │ │
│  │  │  │  │                默认64MB/128MB                      │  │  │  │ │
│  │  │  │  │                容易溢出                            │  │  │  │ │
│  │  │  │  │  大小限制      无固定上限, 受系统内存限制            │  │  │  │ │
│  │  │  │  │  垃圾回收      伴随 Full GC                        │  │  │  │ │
│  │  │  │  │                效率低                              │  │  │  │ │
│  │  │  │  │  垃圾回收      类卸载时回收, 独立于 GC              │  │  │  │ │
│  │  │  │  │                效率高                              │  │  │  │ │
│  │  │  │  │  参数          -XX:PermSize/-XX:MaxPermSize         │  │  │  │ │
│  │  │  │  │                -XX:MaxPermSize 必须设置             │  │  │  │ │
│  │  │  │  │  参数          -XX:MetaspaceSize                    │  │  │  │ │
│  │  │  │  │                -XX:MaxMetaspaceSize                │  │  │  │ │
│  │  │  │  │                默认无限制                          │  │  │  │ │
│  │  │  │  │  OOM 消息      java.lang.OutOfMemoryError:         │  │  │  │ │
│  │  │  │  │                PermGen space                       │  │  │  │ │
│  │  │  │  │  OOM 消息      java.lang.OutOfMemoryError:         │  │  │  │ │
│  │  │  │  │                Metaspace                           │  │  │  │ │
│  │  │  │  └───────────────────────────────────────────────────┘  │  │  │ │
│  │  │  └─────────────────────────────────────────────────────────┘  │  │ │
│  │  │                                                               │  │ │
│  │  │  方法区特点:                                                    │  │ │
│  │  │  ├── 所有线程共享                                             │  │ │
│  │  │  ├── 存放已被虚拟机加载的类信息、常量、静态变量等                 │  │ │
│  │  │  └── JDK 7及以前叫永久代(PermGen), JDK 8+叫元空间(Metaspace)    │  │ │
│  │  └───────────────────────────────────────────────────────────────┘  │ │
│  │                                                                     │ │
│  │  ┌───────────────────────────────────────────────────────────────┐  │ │
│  │  │                      直接内存 (Direct Memory)                │  │ │
│  │  │                                                               │  │ │
│  │  │  作用: NIO 使用的堆外内存, 通过 DirectByteBuffer 访问          │  │ │
│  │  │  特点: 不在 JVM 堆内, 受本机物理内存限制                      │  │ │
│  │  │  OOM: java.lang.OutOfMemoryError: Direct buffer memory       │  │ │
│  │  │  参数: -XX:MaxDirectMemorySize                               │  │ │
│  │  └───────────────────────────────────────────────────────────────┘  │ │
│  └─────────────────────────────────────────────────────────────────────┘ │
│                                                                          │
└──────────────────────────────────────────────────────────────────────────┘

三、各内存区域详解

3.1 程序计数器(Program Counter)

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│                    程序计数器                                    │
│                                                                 │
│  📍 作用:                                                        │
│  ├── 记录当前线程正在执行的字节码指令地址(行号)                    │
│  ├── 分支、循环、跳转、异常处理、线程恢复都依赖它                    │
│  └── 如果执行 native 方法, 计数器值为空 (Undefined)               │
│                                                                 │
│  🎯 特点:                                                        │
│  ├── 线程私有(每个线程都有独立的程序计数器)                       │
│  ├── 占用内存极小, 可忽略不计                                     │
│  └── JVM 规范中唯一没有规定 OOM 的区域                            │
│                                                                 │
│  💡 面试要点:                                                    │
│  ├── 为什么是线程私有?                                            │
│  │   → 多线程执行时, 线程切换后需要恢复到正确的执行位置              │
│  │   → 每个线程需要独立的计数器来记录自己的执行位置                 │
│  │                                                               │
│  └── 为什么不会 OOM?                                              │
│      → 计数器只需要记录当前指令地址, 大小固定                       │
│      → 不会随着程序执行而增长                                     │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3.2 虚拟机栈(Java Virtual Machine Stack)

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│                    虚拟机栈                                      │
│                                                                 │
│  📍 作用:                                                        │
│  ├── 描述 Java 方法执行的内存模型                                 │
│  ├── 每个方法调用都会创建一个栈帧 (Stack Frame)                   │
│  └── 栈帧入栈/出栈对应方法的调用/返回                             │
│                                                                 │
│  📐 栈帧结构:                                                    │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │                    栈帧 (Stack Frame)                    │  │
│  │                                                          │  │
│  │  1. 局部变量表 (Local Variable Table)                    │  │
│  │     ├── 存储方法参数和局部变量                              │  │
│  │     ├── 以 Slot 为单位, 默认 32 位                        │  │
│  │     ├── long/double 占用 2 个 Slot                       │  │
│  │     └── this 引用在实例方法中占据第 0 位                    │  │
│  │                                                          │  │
│  │  2. 操作数栈 (Operand Stack)                              │  │
│  │     ├── 作为方法执行的临时工作区                            │  │
│  │     ├── 存放计算过程中的中间结果                            │  │
│  │     └── 方法调用时传递参数                                  │  │
│  │                                                          │  │
│  │  3. 动态链接 (Dynamic Linking)                            │  │
│  │     ├── 指向运行时常量池中该方法的符号引用                   │  │
│  │     └── 运行时解析为直接引用                                │  │
│  │                                                          │  │
│  │  4. 方法返回地址 (Return Address)                         │  │
│  │     ├── 存放调用该方法前的程序计数器值                       │  │
│  │     └── 方法执行完后返回到调用者                            │  │
│  │                                                          │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  🎯 特点:                                                        │
│  ├── 线程私有                                                   │
│  ├── LIFO (后进先出)                                            │
│  ├── 栈深度固定 (由 -Xss 参数控制)                               │
│  └── 栈帧内存随方法调用分配, 方法返回释放                         │
│                                                                 │
│  ⚠️ 异常场景:                                                    │
│  ├── StackOverflowError:                                        │
│  │   → 原因: 栈深度超过虚拟机允许的最大深度                       │
│  │   → 场景: 无限递归调用                                        │
│  │   → 参数: -Xss 控制栈大小                                     │
│  │                                                               │
│  └── OutOfMemoryError:                                          │
│      → 原因: 无法创建新的线程栈                                   │
│      → 场景: 创建大量线程                                         │
│      → 参数: -Xss 越小, 可创建线程越多                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3.3 本地方法栈(Native Method Stack)

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│                    本地方法栈                                    │
│                                                                 │
│  📍 作用:                                                        │
│  ├── 为 native 方法提供栈空间                                    │
│  ├── native 方法是用非 Java 语言编写的方法                        │
│  └── 如 JNI (Java Native Interface) 调用的 C/C++ 方法           │
│                                                                 │
│  🎯 特点:                                                        │
│  ├── 与虚拟机栈类似, 但服务于 native 方法                         │
│  ├── 线程私有                                                   │
│  └── Sun HotSpot VM 将其与虚拟机栈合二为一                       │
│                                                                 │
│  ⚠️ 异常场景:                                                    │
│  ├── StackOverflowError: native 方法执行时递归太深               │
│  └── OutOfMemoryError: 无法为新的 native 方法分配栈空间          │
│                                                                 │
│  ⚙️ 参数:                                                        │
│  └── -Xoss: 设置本地方法栈大小 (HotSpot 中无效)                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3.4 堆(Heap)

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│                       堆                                         │
│                                                                 │
│  📍 作用:                                                        │
│  ├── 存放对象实例和数组                                           │
│  ├── GC (垃圾回收) 的主要战场                                     │
│  └── 几乎所有对象都在这里分配内存                                  │
│                                                                 │
│  📐 分代结构:                                                    │
│                                                                 │
│  新生代 (约占堆的1/3):                                            │
│  ├── Eden: 新对象分配区 (约80%)                                   │
│  │   → 大部分对象在这里创建                                        │
│  │   → Minor GC 时, 存活对象进入 Survivor                        │
│  │                                                               │
│  ├── S0 (From): 幸存者区0 (约10%)                                 │
│  │   → 存放 Minor GC 后存活的对象                                 │
│  │                                                               │
│  └── S1 (To): 幸存者区1 (约10%)                                   │
│      → 与 S0 交替使用, 始终保持为空作为复制目标                    │
│                                                                 │
│  老年代 (约占堆的2/3):                                            │
│  ├── 存放存活时间长的对象                                          │
│  ├── 新生代对象年龄达到阈值 (-XX:MaxTenuringThreshold) 后进入      │
│  ├── 大对象直接进入 (-XX:PretenureSizeThreshold)                 │
│  └── Major GC/Full GC 的主要区域                                  │
│                                                                 │
│  🗑️ GC 类型:                                                     │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  GC 类型        触发条件          回收区域        耗时     │  │
│  │  ──────────    ──────────        ──────────      ──────   │  │
│  │  Minor GC      Eden 满            新生代          快       │  │
│  │  Major GC      老年代满           老年代          慢       │  │
│  │  Full GC       系统触发           全堆+方法区      很慢     │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  🎯 特点:                                                        │
│  ├── 所有线程共享                                                │
│  ├── JVM 中最大的一块内存区域                                     │
│  ├── 可通过 -Xms/-Xmx 参数调整大小                                │
│  └── 对象分配使用指针碰撞或空闲列表算法                            │
│                                                                 │
│  ⚠️ OOM 场景:                                                    │
│  ├── 错误: java.lang.OutOfMemoryError: Java heap space          │
│  ├── 原因: 堆内存不足, 无法为新对象分配空间                       │
│  └── 场景:                                                       │
│      ├── 创建大量大对象                                           │
│      ├── 内存泄漏 (对象引用未释放)                                │
│      ├── 无限循环创建对象                                         │
│      └── 堆设置过小                                               │
│                                                                 │
│  ⚙️ 参数:                                                        │
│  ├── -Xms: 堆最小容量 (默认物理内存的1/64)                        │
│  ├── -Xmx: 堆最大容量 (默认物理内存的1/4)                        │
│  ├── -XX:NewRatio: 新生代/老年代比例 (默认2)                      │
│  ├── -XX:SurvivorRatio: Eden/Survivor 比例 (默认8)               │
│  └── -XX:MaxTenuringThreshold: 对象晋升年龄阈值 (默认15)          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3.5 方法区(Method Area)

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│                     方法区                                       │
│                                                                 │
│  📍 作用:                                                        │
│  ├── 存储已被虚拟机加载的类信息                                   │
│  ├── 存储常量、静态变量、即时编译器编译后的代码                    │
│  └── 运行时常量池是方法区的一部分                                 │
│                                                                 │
│  📐 组成:                                                        │
│                                                                 │
│  1. 运行时常量池:                                                │
│     ├── 字面量 (字符串、final常量值)                              │
│     ├── 符号引用 (类、方法、字段的全限定名)                         │
│     ├── 直接引用 (解析后的内存地址)                               │
│     └── JDK 7+ 字符串常量移至堆中                                 │
│                                                                 │
│  2. 类信息:                                                      │
│     ├── 类的全限定名                                             │
│     ├── 父类的全限定名                                            │
│     ├── 访问修饰符 (public, abstract, final)                     │
│     └── 接口列表                                                 │
│                                                                 │
│  3. 字段信息:                                                    │
│     ├── 字段名称                                                 │
│     ├── 字段类型                                                 │
│     └── 字段修饰符                                                │
│                                                                 │
│  4. 方法信息:                                                    │
│     ├── 方法名称                                                 │
│     ├── 方法参数列表                                             │
│     ├── 返回值类型                                               │
│     ├── 方法修饰符                                                │
│     └── 方法字节码                                               │
│                                                                 │
│  🔄 JDK 7 vs JDK 8+ 变化:                                        │
│                                                                 │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │  对比维度         PermGen (JDK 7)      Metaspace (JDK 8+) │  │
│  │  ─────────────   ──────────────       ────────────────── │  │
│  │  内存位置        堆内存内部             本地内存 (Native)  │  │
│  │  大小限制        固定大小               无固定上限          │  │
│  │  默认大小        64MB (32位)           受系统内存限制      │  │
│  │                 128MB (64位)                              │  │
│  │  GC 时机         Full GC 时回收        类卸载时独立回收    │  │
│  │  OOM 消息        PermGen space        Metaspace           │  │
│  │  参数            -XX:PermSize          -XX:MetaspaceSize  │  │
│  │                 -XX:MaxPermSize       -XX:MaxMetaspaceSize│  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  ⚠️ OOM 场景:                                                    │
│                                                                 │
│  JDK 7:                                                         │
│  ├── 错误: java.lang.OutOfMemoryError: PermGen space            │
│  └── 场景:                                                       │
│      ├── 大量动态生成类 (如反射、动态代理、CGLIB)                 │
│      ├── String.intern() 调用过多                                │
│      └── 热部署频繁重新加载类                                     │
│                                                                 │
│  JDK 8+:                                                        │
│  ├── 错误: java.lang.OutOfMemoryError: Metaspace                │
│  └── 场景:                                                       │
│      ├── 大量动态生成类 (如反射、动态代理、CGLIB)                 │
│      ├── ASM 字节码生成工具频繁使用                               │
│      └── 框架大量使用注解处理器                                   │
│                                                                 │
│  ⚙️ 参数 (JDK 8+):                                               │
│  ├── -XX:MetaspaceSize: 元空间初始大小 (默认约21MB)               │
│  ├── -XX:MaxMetaspaceSize: 元空间最大大小 (默认无限制)            │
│  └── -XX:CompressedClassSpaceSize: 压缩类空间大小 (默认1GB)       │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3.6 直接内存(Direct Memory)

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│                    直接内存                                       │
│                                                                 │
│  📍 作用:                                                        │
│  ├── NIO 使用的堆外内存                                          │
│  ├── 通过 DirectByteBuffer 类访问                                 │
│  └── 避免 Java 堆和 Native 堆之间的数据复制                       │
│                                                                 │
│  🎯 特点:                                                        │
│  ├── 不在 JVM 规范定义的运行时数据区域内                          │
│  ├── 不属于堆内存, 属于本地内存                                   │
│  ├── 受本机物理内存限制                                          │
│  └── 速度比堆内存快 (减少一次数据复制)                            │
│                                                                 │
│  ⚠️ OOM 场景:                                                    │
│  ├── 错误: java.lang.OutOfMemoryError: Direct buffer memory     │
│  ├── 原因: 系统内存不足, 无法分配直接内存                          │
│  └── 场景:                                                       │
│      ├── NIO 程序分配大量 DirectByteBuffer                        │
│      ├── -Xmx 设置过大, 挤占系统内存                              │
│      └── 直接内存使用后未及时释放                                 │
│                                                                 │
│  ⚙️ 参数:                                                        │
│  └── -XX:MaxDirectMemorySize: 最大直接内存大小 (默认等于 -Xmx)    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

四、OOM 速查表

┌─────────────────────────────────────────────────────────────────────────────────────┐
│                                                                                     │
│                    JVM 各区域 OOM 速查表                                            │
│                                                                                     │
│  ┌───────┬─────────────────────────────────────┬────────────────────────────────┐  │
│  │ 区域  │ 错误信息                             │ 触发场景                        │  │
│  ├───────┼─────────────────────────────────────┼────────────────────────────────┤  │
│  │ 程序计数器│ 无(JVM 规范未规定)              │ 无                              │  │
│  ├───────┼─────────────────────────────────────┼────────────────────────────────┤  │
│  │ 虚拟机栈 │ java.lang.StackOverflowError      │ 无限递归调用                    │  │
│  │       │                                     │ 栈帧过大                        │  │
│  │       │ java.lang.OutOfMemoryError           │ 创建大量线程                    │  │
│  ├───────┼─────────────────────────────────────┼────────────────────────────────┤  │
│  │ 本地方法栈│ java.lang.StackOverflowError     │ native 方法递归太深             │  │
│  │       │ java.lang.OutOfMemoryError           │ 无法分配 native 方法栈空间      │  │
│  ├───────┼─────────────────────────────────────┼────────────────────────────────┤  │
│  │ 堆    │ java.lang.OutOfMemoryError:         │ 创建大量大对象                  │  │
│  │       │   Java heap space                    │ 内存泄漏                        │  │
│  │       │                                     │ 无限循环创建对象                │  │
│  │       │                                     │ 堆设置过小                      │  │
│  ├───────┼─────────────────────────────────────┼────────────────────────────────┤  │
│  │ 方法区 │ java.lang.OutOfMemoryError:         │ 大量动态生成类 (反射/CGLIB)     │  │
│  │ (JDK 7)│   PermGen space                    │ String.intern() 过多           │  │
│  │       │                                     │ 热部署频繁                      │  │
│  │       ├─────────────────────────────────────┼────────────────────────────────┤  │
│  │ 方法区 │ java.lang.OutOfMemoryError:         │ 大量动态生成类 (反射/CGLIB)     │  │
│  │ (JDK 8+)│   Metaspace                       │ ASM 字节码生成                 │  │
│  │       │                                     │ 注解处理器大量使用              │  │
│  ├───────┼─────────────────────────────────────┼────────────────────────────────┤  │
│  │ 直接内存│ java.lang.OutOfMemoryError:         │ NIO 分配大量 DirectByteBuffer   │  │
│  │       │   Direct buffer memory              │ -Xmx 设置过大                  │  │
│  │       │                                     │ 直接内存未及时释放              │  │
│  └───────┴─────────────────────────────────────┴────────────────────────────────┘  │
│                                                                                     │
│  ┌───────┬──────────────────────────────┬─────────────────────────────────────┐  │
│  │ 区域  │ JVM 参数                      │ 修复策略                           │  │
│  ├───────┼──────────────────────────────┼─────────────────────────────────────┤  │
│  │ 程序计数器│ 无                         │ 无需处理                           │  │
│  ├───────┼──────────────────────────────┼─────────────────────────────────────┤  │
│  │ 虚拟机栈 │ -Xss (默认1MB)              │ 检查递归深度                       │  │
│  │       │                              │ 增加 -Xss 值                      │  │
│  │       │                              │ 减少线程数量                       │  │
│  ├───────┼──────────────────────────────┼─────────────────────────────────────┤  │
│  │ 本地方法栈│ -Xoss (HotSpot 无效)       │ 检查 native 方法实现               │  │
│  ├───────┼──────────────────────────────┼─────────────────────────────────────┤  │
│  │ 堆    │ -Xms/-Xmx                     │ 增加堆大小                         │  │
│  │       │ -XX:NewRatio                  │ 排查内存泄漏                       │  │
│  │       │ -XX:SurvivorRatio             │ 优化对象生命周期                   │  │
│  │       │                              │ 使用内存分析工具 (MAT/JProfiler)    │  │
│  ├───────┼──────────────────────────────┼─────────────────────────────────────┤  │
│  │ 方法区 │ -XX:PermSize/-XX:MaxPermSize │ 增加永久代大小 (JDK 7)             │  │
│  │ (JDK 7)│                              │ 升级到 JDK 8+                      │  │
│  │       │                              │ 减少动态类生成                     │  │
│  │       ├──────────────────────────────┼─────────────────────────────────────┤  │
│  │ 方法区 │ -XX:MetaspaceSize            │ 设置 -XX:MaxMetaspaceSize          │  │
│  │ (JDK 8+)│ -XX:MaxMetaspaceSize       │ 减少动态类生成                     │  │
│  │       │                              │ 使用 ClassLoader 缓存               │  │
│  ├───────┼──────────────────────────────┼─────────────────────────────────────┤  │
│  │ 直接内存│ -XX:MaxDirectMemorySize     │ 减少 DirectByteBuffer 使用         │  │
│  │       │                              │ 及时释放直接内存                   │  │
│  │       │                              │ 合理设置 -Xmx 大小                 │  │
│  └───────┴──────────────────────────────┴─────────────────────────────────────┘  │
│                                                                                     │
└─────────────────────────────────────────────────────────────────────────────────────┘

五、关键面试问题

5.1 为什么堆要分代?

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│                    为什么堆要分代?                              │
│                                                                 │
│  📊 分代依据:                                                    │
│  ├── 弱分代假说: 大部分对象朝生夕死                               │
│  └── 强分代假说: 熬过多次 GC 的对象更难被回收                     │
│                                                                 │
│  🎯 分代优势:                                                    │
│                                                                 │
│  1. 提高 GC 效率:                                                │
│     ├── 新生代: 复制算法, 只需要复制少量存活对象                   │
│     ├── 老年代: 标记-清除/整理, 减少内存碎片                       │
│     └── 分代后, Minor GC 只回收新生代, 速度快                     │
│                                                                 │
│  2. 减少停顿时间:                                                │
│     ├── Minor GC 频繁但快速                                      │
│     ├── Major GC 不频繁但慢                                      │
│     └── 用户感知主要来自 Major GC                                 │
│                                                                 │
│  3. 针对性优化:                                                  │
│     ├── 新生代: 适合复制算法                                      │
│     └── 老年代: 适合标记-清除/整理算法                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

5.2 对象分配流程

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│                    对象分配流程                                  │
│                                                                 │
│  1. 检查是否是大对象                                              │
│     ├── 是 → 直接进入老年代 (-XX:PretenureSizeThreshold)          │
│     └── 否 → 进入下一步                                          │
│                                                                 │
│  2. 检查新生代是否有足够空间                                        │
│     ├── 有 → 分配到 Eden                                          │
│     └── 否 → 执行 Minor GC                                        │
│                                                                 │
│  3. Minor GC 后检查                                               │
│     ├── 存活对象能放入 Survivor → 放入 S0/S1                      │
│     └── 不能放入 → 通过分配担保机制进入老年代                        │
│                                                                 │
│  4. 对象在 Survivor 中复制                                         │
│     ├── 每次 Minor GC, 对象年龄+1                                 │
│     ├── 年龄达到阈值 (-XX:MaxTenuringThreshold) → 进入老年代       │
│     └── Survivor 空间不足 → 直接进入老年代                         │
│                                                                 │
│  📋 流程图:                                                      │
│                                                                 │
│  new Object()                                                   │
│      │                                                          │
│      ▼                                                          │
│  ┌──────────────┐                                               │
│  │ 大对象检查   │──是──→ 老年代                                   │
│  └──────┬───────┘                                               │
│         │ 否                                                    │
│         ▼                                                       │
│  ┌──────────────┐                                               │
│  │ Eden 有空间? │──是──→ 分配到 Eden                             │
│  └──────┬───────┘                                               │
│         │ 否                                                    │
│         ▼                                                       │
│  ┌──────────────┐                                               │
│  │ Minor GC     │                                               │
│  └──────┬───────┘                                               │
│         │                                                       │
│         ▼                                                       │
│  ┌──────────────┐                                               │
│  │ 存活对象放入  │──→ 年龄达标? ──是──→ 老年代                     │
│  │ Survivor     │                  │                            │
│  └──────────────┘                  │ 否                         │
│                                    ▼                            │
│                              继续在 Survivor 中复制               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

5.3 JDK 7 到 JDK 8 的内存模型变化

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│              JDK 7 → JDK 8 内存模型变化                          │
│                                                                 │
│  📌 主要变化:                                                    │
│                                                                 │
│  1. 永久代 (PermGen) → 元空间 (Metaspace)                        │
│     ├── 内存位置: 堆内 → 本地内存                                  │
│     ├── 大小限制: 固定 → 无上限                                    │
│     └── GC 效率: 低 → 高                                         │
│                                                                 │
│  2. 字符串常量池移至堆中                                          │
│     ├── JDK 7: 字符串常量在永久代                                 │
│     ├── JDK 8+: 字符串常量在堆中                                  │
│     └── 影响: String.intern() 不再导致 PermGen OOM               │
│                                                                 │
│  3. 静态变量移至堆中                                              │
│     ├── JDK 7: 静态变量在永久代                                   │
│     └── JDK 8+: 静态变量在堆中 (所属类的 Class 对象中)            │
│                                                                 │
│  🎯 变化原因:                                                    │
│                                                                 │
│  1. 解决永久代 OOM 问题                                           │
│     ├── 永久代大小固定, 容易溢出                                  │
│     ├── 元空间使用本地内存, 理论上不会溢出                          │
│                                                                 │
│  2. 提高 GC 效率                                                 │
│     ├── 永久代回收需要 Full GC                                    │
│     ├── 元空间可以独立回收, 不影响堆                              │
│                                                                 │
│  3. 简化 JVM 实现                                                 │
│     ├── 移除永久代, 减少内存管理复杂性                            │
│     └── 统一使用本地内存管理                                      │
│                                                                 │
│  ⚠️ 迁移注意事项:                                                 │
│                                                                 │
│  ├── 移除 -XX:PermSize/-XX:MaxPermSize 参数                      │
│  ├── 添加 -XX:MetaspaceSize/-XX:MaxMetaspaceSize 参数            │
│  ├── String.intern() 行为变化                                    │
│  └── 热部署方案需要调整                                          │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

六、实战:各区域 OOM 触发示例

我为你准备了一个专门用于触发各区域 OOM 的示例工程:

6.1 项目结构

jvm-oom-demo/
├── src/main/java/com/example/demo/
│   ├── StackOverflowDemo.java      # 栈溢出
│   ├── HeapOOMDemo.java            # 堆溢出
│   ├── MetaspaceOOMDemo.java       # 元空间溢出
│   ├── DirectMemoryOOMDemo.java    # 直接内存溢出
│   └── ThreadOOMDemo.java          # 线程过多导致 OOM
└── pom.xml

6.2 触发命令示例

# 1. 栈溢出 (-Xss256k 减小栈大小)
java -Xss256k com.example.demo.StackOverflowDemo

# 2. 堆溢出 (-Xmx32m 减小堆大小)
java -Xmx32m com.example.demo.HeapOOMDemo

# 3. 元空间溢出 (-XX:MaxMetaspaceSize=32m)
java -XX:MaxMetaspaceSize=32m com.example.demo.MetaspaceOOMDemo

# 4. 直接内存溢出 (-XX:MaxDirectMemorySize=32m)
java -XX:MaxDirectMemorySize=32m com.example.demo.DirectMemoryOOMDemo

# 5. 线程过多导致 OOM (-Xss128k 减小每个线程栈大小)
java -Xss128k com.example.demo.ThreadOOMDemo

6.3 代码示例

栈溢出

public class StackOverflowDemo {
    private static int depth = 0;
    
    public static void recursiveCall() {
        depth++;
        System.out.println("递归深度: " + depth);
        recursiveCall();
    }
    
    public static void main(String[] args) {
        recursiveCall();
    }
}

堆溢出

public class HeapOOMDemo {
    public static void main(String[] args) {
        List<Object> list = new ArrayList<>();
        int count = 0;
        while (true) {
            list.add(new byte[1024 * 1024]);
            System.out.println("已分配: " + ++count + "MB");
        }
    }
}

元空间溢出

public class MetaspaceOOMDemo {
    public static void main(String[] args) throws Exception {
        int count = 0;
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(Object.class);
            enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));
            enhancer.create();
            System.out.println("已创建类: " + ++count);
        }
    }
}

七、总结

7.1 内存模型核心要点

┌─────────────────────────────────────────────────────────────────┐
│                                                                 │
│                    内存模型核心要点                              │
│                                                                 │
│  线程私有区域:                                                    │
│  ├── 程序计数器: 记录指令地址, 唯一不 OOM                         │
│  ├── 虚拟机栈: 方法执行栈, 栈帧=局部变量表+操作数栈+动态链接+返回地址│
│  └── 本地方法栈: native 方法栈                                    │
│                                                                 │
│  线程共享区域:                                                    │
│  ├── 堆: 对象分配区, 分新生代(Eden/S0/S1)和老年代                  │
│  ├── 方法区: 类信息+常量池, JDK 7 PermGen → JDK 8+ Metaspace      │
│  └── 直接内存: NIO 堆外内存                                       │
│                                                                 │
│  OOM 速记:                                                       │
│  ├── StackOverflowError: 栈深度超限 (递归太深)                    │
│  ├── Java heap space: 堆内存不足                                 │
│  ├── PermGen space: JDK 7 永久代不足                             │
│  ├── Metaspace: JDK 8+ 元空间不足                                │
│  ├── Direct buffer memory: 直接内存不足                          │
│  └── 创建线程失败: 线程栈占用过多内存                              │
│                                                                 │
│  JDK 8+ 关键参数:                                                 │
│  ├── -Xms/-Xmx: 堆大小                                           │
│  ├── -Xss: 栈大小                                                │
│  ├── -XX:MetaspaceSize/-XX:MaxMetaspaceSize: 元空间大小           │
│  └── -XX:MaxDirectMemorySize: 直接内存大小                        │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

7.2 面试准备清单

✅ 理解每个内存区域的作用和特点
✅ 掌握栈帧的四个组成部分
✅ 理解堆的分代结构和 GC 流程
✅ 知道 JDK 7 和 JDK 8+ 的内存模型差异
✅ 能说出每个区域的 OOM 场景和原因
✅ 熟悉常用的 JVM 参数
✅ 了解对象分配流程
✅ 知道为什么堆要分代

💡 互动话题:你在面试中遇到过哪些关于 JVM 内存模型的问题?有没有被问到过特别刁钻的问题?欢迎在评论区分享你的经历!


标题:面试必问的 JVM 内存模型,一张图画清楚
作者:jiangyi
地址:http://jiangyi.space/articles/2026/07/27/1785060795141.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消