
JVM知识梳理作者没有四次元口袋的蓝胖日期2026-07-30标签Java, JVM, 运行时数据区, 堆, 栈, 方法区JVM 内存模型是 Java 面试的必考题。重点掌握堆、栈、方法区三个核心区域其他区域了解即可。一、整体划分JVM 运行时数据区分为线程私有和线程共享两部分JVM 运行时数据区 ├── 线程私有每个线程独立随线程创建/销毁 │ ├── 程序计数器PC Register │ ├── 虚拟机栈VM Stack │ └── 本地方法栈Native Method Stack ├── 线程共享所有线程共用JVM 启动时创建 │ ├── 堆Heap │ └── 方法区Method Area └── 直接内存Direct MemoryNIO 使用不属于 JVM 运行时数据区面试关键区分线程私有 → 随线程生灭不会有并发问题线程共享 → 需要考虑线程安全和 GC二、虚拟机栈VM Stack— 重点2.1 作用存储方法的局部变量表、操作数栈、动态链接、方法出口地址。每次方法调用都会在栈中创建一个栈帧方法执行完毕栈帧出栈。┌─────────────────────────────┐ │ 方法调用 3 │ ← 栈顶当前执行的方法 │ ┌─────────────────────┐ │ │ │ 栈帧Frame │ │ │ │ - 局部变量表 │ │ │ │ - 操作数栈 │ │ │ │ - 动态链接 │ │ │ │ - 返回地址 │ │ │ └─────────────────────┘ │ ├─────────────────────────────┤ │ 方法调用 2 │ ├─────────────────────────────┤ │ 方法调用 1 │ ← 栈底 └─────────────────────────────┘2.2 局部变量表存储方法内的局部变量基本数据类型int、long、float、double、boolean、char、short、byte对象引用reference指向堆中对象的指针returnAddress 类型publicvoidtest(){inta10;// 局部变量表存 aintb20;// 局部变量表存 bObjectobjnewObject();// 局部变量表存引用对象在堆中}2.3 常见异常异常原因StackOverflowError栈深度超过限制递归太深、死循环调用OutOfMemoryError动态扩展栈时无法申请到足够内存2.4 面试高频问题Q栈和堆的区别维度栈堆存储内容局部变量、方法调用对象实例、数组线程私有是否所有线程共享生命周期随方法调用由 GC 管理分配速度快指针移动慢需要查找空闲内存大小较小默认 1MB较大默认物理内存的 1/4三、堆Heap— 重点3.1 作用存储对象实例和数组是 GC垃圾回收的主要区域。ObjectobjnewObject();// 引用在栈中对象在堆中3.2 堆的分代结构现代 JVM 把堆分为新生代和老生代堆Heap ├── 新生代Young Generation—— 占 1/3 │ ├── Eden 区80% │ ├── Survivor From10% │ └── Survivor To10% └── 老生代Old Generation—— 占 2/3对象生命周期新对象 → Eden 区 ↓Eden 满Minor GC Survivor From/To复制存活对象 ↓年龄达到阈值默认 15 老生代 ↓老生代满Major GC / Full GC 如果还是放不下 → OutOfMemoryError3.3 为什么分代新生代对象朝生夕死GC 频繁但每次回收量小复制算法效率高老生代对象存活率高GC 较少但每次回收量大标记-清除/标记-整理面试回答模板分代是为了提高 GC 效率。新生代用复制算法每次只复制少量存活对象老生代用标记-清除或标记-整理适合长期存活的对象。3.4 常见异常异常原因OutOfMemoryError堆内存不足对象太多、内存泄漏# JVM 参数调整堆大小-Xms512m# 初始堆大小-Xmx2048m# 最大堆大小四、方法区Method Area— 重点4.1 作用存储类信息、常量、静态变量、JIT 编译后的代码。注意方法区是规范定义的概念不同 JVM 实现不同。HotSpot 用永久代JDK 8 之前或元空间JDK 8实现。4.2 JDK 8 前 vs JDK 8版本实现方式位置JDK 7 及之前永久代PermGenJVM 进程内存JDK 8元空间Metaspace本地内存Native Memory为什么要改永久代大小固定容易 OOM元空间使用本地内存理论上不受 JVM 内存限制方便 HotSpot 与 JRockit 融合JRockit 没有永久代4.3 方法区存储内容方法区元空间 ├── 类信息Class类名、父类、接口、访问修饰符 ├── 字段信息Field字段名、类型、修饰符 ├── 方法信息Method方法名、参数、返回类型、字节码 ├── 常量池Constant Pool字面量、符号引用 ├── 静态变量Static Variables └── JIT 编译后的代码4.4 运行时常量池方法区中有一个运行时常量池是类加载后生成的。Strings1abc;// abc 在运行时常量池Strings2abc;// 同一个引用System.out.println(s1s2);// true4.5 常见异常异常原因OutOfMemoryError: Metaspace加载的类太多、元空间不足# JDK 8 调整元空间大小-XX:MetaspaceSize256m-XX:MaxMetaspaceSize512m五、其他区域5.1 程序计数器PC Register作用记录当前线程执行的字节码行号线程私有每个线程独立一个唯一不会 OOM 的区域线程 A 执行到第 10 行 → PC 10 线程 B 执行到第 20 行 → PC 205.2 本地方法栈Native Method Stack作用为 Native 方法服务如 C/C 编写的方法线程私有HotSpot 把本地方法栈和虚拟机栈合二为一5.3 直接内存Direct Memory作用NIO 引入通过DirectByteBuffer分配堆外内存不属于 JVM 运行时数据区避免 Java 堆和 Native 堆之间的数据复制提高 I/O 性能// 堆外内存ByteBufferbufferByteBuffer.allocateDirect(1024);六、各区域对比总结区域线程存储内容异常程序计数器私有字节码行号唯一不会 OOM虚拟机栈私有局部变量、方法调用StackOverflow / OOM本地方法栈私有Native 方法StackOverflow / OOM堆共享对象、数组OOM方法区共享类信息、常量、静态变量OOM直接内存—NIO 堆外内存OOM️ 思维导图速览JVM 运行时数据区 ├── 线程私有 │ ├── 程序计数器字节码行号唯一不会 OOM │ ├── 虚拟机栈栈帧局部变量表/操作数栈/动态链接/返回地址 │ └── 本地方法栈Native 方法 ├── 线程共享 │ ├── 堆对象实例分新生代/老生代 │ │ ├── 新生代Eden Survivor复制算法 │ │ └── 老生代标记-清除/标记-整理 │ └── 方法区JDK 8 用元空间本地内存 │ ├── 类信息、字段、方法 │ ├── 运行时常量池 │ └── 静态变量、JIT 代码 └── 直接内存NIO 堆外内存不属于运行时数据区 写在最后学习建议堆栈方法区必须讲清楚面试必问能说清栈存引用、堆存对象、方法区存类信息就过关分代结构要理解不是死记硬背理解为什么分代——新生代朝生夕死用复制算法老生代存活率高用标记-整理JDK 8 方法区变化要知道永久代 → 元空间原因是永久代大小固定容易 OOM元空间用本地内存更灵活常见异常要对应StackOverflow 对应栈OOM 对应堆/方法区面试会问什么情况下会抛什么异常面试回答模板Q说说 JVM 运行时数据区JVM 运行时数据区分为线程私有和线程共享两部分。线程私有包括程序计数器记录字节码行号唯一不会 OOM 的区域、虚拟机栈存储局部变量和方法调用每次方法调用创建一个栈帧、本地方法栈为 Native 方法服务。线程共享包括堆存储对象实例分新生代和老生代新生代用复制算法回收效率高老生代用标记-整理和方法区JDK 8 之前用永久代之后用元空间存储类信息、常量、静态变量。重点说一下堆和栈的区别栈存局部变量和引用堆存对象实例栈是线程私有的堆是共享的栈分配速度快但空间小堆分配速度慢但空间大。Q为什么堆要分新生代和老生代分代是为了提高 GC 效率。大部分对象朝生夕死放在新生代用复制算法每次只复制少量存活对象效率高少量长期存活的对象进入老生代用标记-清除或标记-整理算法适合存活率高的场景。这样不同区域用不同回收策略整体 GC 效率更高。