一、JVM 是什么
Java Virtual Machine(Java 虚拟机) ,运行 Java 字节码(.class 文件),实现"一次编译,到处运行"的跨平台特性。
Java 源码 (.java) → javac 编译 → 字节码 (.class) → JVM 解析执行 → 操作系统机器码
三者关系:
| 概念 | 组成 |
|---|---|
| JDK | JRE + 开发工具(javac、jar、javadoc 等) |
| JRE | JVM + Java 基础类库(rt.jar 等) |
| JVM | 类加载器 + 运行时数据区 + 执行引擎 |
二、JVM 内存区域(运行时数据区)
整体分为 线程私有 和 线程共享 两大部分。
┌─────────────────────────────────────────────────────┐
│ JVM 运行时数据区 │
├────────────────────┬────────────────────────────────┤
│ 线程私有(每线程一份) │ 线程共享(全局) │
├────────────────────┼────────────────────────────────┤
│ 1. 程序计数器 (PC) │ 1. 堆 (Heap) ------ GC 主战场 │
│ 2. Java 虚拟机栈 │ 2. 元空间 (Metaspace) JDK8+ │
│ 3. 本地方法栈 │ (JDK7-: 永久代 PermGen) │
│ │ 3. 字符串常量池 (JDK7+ 在堆中) │
└────────────────────┴────────────────────────────────┘
2.1 程序计数器(PC Register)
- 作用:记录当前线程正在执行的字节码指令地址(行号)。
- 特点 :唯一不会抛出 OOM 的区域,线程私有,生命周期与线程相同。
2.2 Java 虚拟机栈(JVM Stack)
- 存储单元:栈帧(Stack Frame),每个方法调用对应一个栈帧的压栈/出栈。
- 栈帧组成:
| 组成部分 | 说明 |
|---|---|
| 局部变量表 | 存储方法参数和局部变量,基本类型存值,对象存引用 |
| 操作数栈 | 字节码指令执行时的操作数入栈/出栈 |
| 动态链接 | 指向运行时常量池中该方法的引用 |
| 方法返回地址 | 方法退出后返回到调用者的位置 |
- 异常 :
StackOverflowError:线程请求栈深度 > 虚拟机允许深度(如递归太深)OutOfMemoryError:栈动态扩展时无法申请足够内存
2.3 本地方法栈(Native Method Stack)
- 与虚拟机栈功能类似,专门服务 native 方法(C/C++ 编写)。
- HotSpot 将本地方法栈和虚拟机栈合二为一。
2.4 堆(Heap)------ 最重要
-
所有对象实例和数组 都在堆上分配,是 GC 的主要管理区域。
-
堆的分代结构(JDK8 经典分代模型):
┌──────────────────────────────────────────┐
│ Heap │
├────────────────────┬─────────────────────┤
│ 新生代 Young │ 老年代 Old │
│ ┌────┬─────┬────┐ │ │
│ │Eden│ S0 │ S1 │ │ 大对象 / 长期存活 │
│ │ 8 │ 1 │ 1 │ │ │
│ └────┴─────┴────┘ │ │
├────────────────────┴─────────────────────┤
│ 比例: Eden : S0 : S1 = 8 : 1 : 1 │
└──────────────────────────────────────────┘ -
JDK8 变化:移除永久代(PermGen),元空间移至本地内存。
2.5 元空间(Metaspace, JDK8+)
| 对比项 | JDK7 及以前(永久代) | JDK8+(元空间) |
|---|---|---|
| 存储位置 | 堆内存(JVM 管理) | 本地内存(操作系统管理) |
| 存储内容 | 类信息、常量、静态变量 | 类信息、常量、静态变量、即时编译代码 |
| 大小限制 | -XX:MaxPermSize |
-XX:MaxMetaspaceSize,默认无上限 |
| OOM 原因 | 类/常量太多 | 类加载过多 |
注意:字符串常量池在 JDK7 移到堆中,JDK8 依然在堆中。
三、对象创建与内存布局
3.1 对象内存布局
┌──────────────┬──────────────┬──────────────┐
│ 对象头 │ 实例数据 │ 对齐填充 │
│ Object Header│ Instance Data│ Padding │
└──────────────┴──────────────┴──────────────┘
对象头包含:
| 组成部分 | 内容 |
|---|---|
| Mark Word | 哈希码(HashCode)、GC 分代年龄、锁状态标志、偏向锁线程 ID 等 |
| 类型指针 | 指向元空间中类的 Class 对象 |
| 数组长度 | 仅数组对象有此字段 |
对齐填充 :保证对象大小是 8 字节的整数倍。
3.2 对象访问方式
| 方式 | 原理 | 特点 |
|---|---|---|
| 句柄访问 | 栈 → 句柄池 → (实例数据指针 + 类型数据指针) | 对象移动时只改句柄,稳定 |
| 直接指针(HotSpot) | 栈 → (实例数据 + 类型指针) | 速度快,HotSpot 默认使用 |
四、垃圾回收(GC)
4.1 判断对象死亡
引用计数法
- 对象被引用 +1,引用失效 -1,计数为 0 则回收。
- 缺点 :无法解决循环引用问题(A ↔ B 互相引用,计数永远不会为 0)。
- HotSpot 不使用此方法。
可达性分析(HotSpot 使用)
以 GC Roots 为起点,沿引用链向下搜索,不可达的对象判定为"垃圾"。
GC Roots 包括:
- 虚拟机栈(局部变量表)中引用的对象
- 方法区中静态变量引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI(Native 方法)引用的对象
- 被同步锁(synchronized)持有的对象
四大引用类型
| 引用类型 | 回收时机 | 用途 |
|---|---|---|
强引用 Object o = new Object() |
永不回收(除非不再被引用) | 普通对象 |
软引用 SoftReference |
内存不足时回收 | 缓存(如图片缓存) |
弱引用 WeakReference |
下次 GC 一定回收 | WeakHashMap、ThreadLocal |
虚引用 PhantomReference |
任何时候,仅做通知 | 对象回收跟踪、NIO 直接内存管理 |
4.2 分代收集思想
| 分代 | 特点 | 回收频率 | GC 类型 |
|---|---|---|---|
| 新生代(Young) | 对象存活短,大批快速死亡 | 高频 | Minor GC |
| 老年代(Old) | 存活久的大对象 | 低频 | Major GC |
对象分配与晋升流程:
1. 新对象 → Eden 区分配
2. Eden 满 → Minor GC,存活对象 → Survivor 区(S0 / S1,轮换使用)
3. 每熬过一次 Minor GC,年龄 +1
4. 年龄达到 15(默认)→ 晋升老年代
5. 大对象 → 直接进入老年代(-XX:PretenureSizeThreshold)
GC 类型对比:
| GC 类型 | 回收区域 | 特点 |
|---|---|---|
| Minor GC | 仅新生代 | 频繁、速度快 |
| Major GC | 仅老年代 | 通常伴随 Minor GC |
| Full GC | 整个堆 + 元空间 | STW 时间长,应尽量避免 |
4.3 垃圾收集算法
| 算法 | 过程 | 优点 | 缺点 | 适用区域 |
|---|---|---|---|---|
| 标记-清除 | 标记垃圾对象 → 直接清除 | 简单 | 产生内存碎片 | 老年代(CMS 基础) |
| 复制算法 | 存活对象复制到新区域 → 清空旧区域 | 无碎片、高效 | 浪费一半内存 | 新生代 |
| 标记-整理 | 标记存活 → 向一端移动 → 清理边界外 | 无碎片 | 移动成本高、STW 长 | 老年代 |
4.4 常见垃圾收集器(⭐ 面试重点)
经典收集器一览
新生代 老年代
┌─────────────┐ ┌─────────────┐
│ Serial │ ──────→ │ Serial Old │ 单线程,Client 模式
│ ParNew │ ──────→ │ CMS │ ParNew + CMS 经典组合
│ Parallel │ ──────→ │ Parallel Old│ JDK8 默认,吞吐量优先
│ Scavenge │ │ │
└─────────────┘ └─────────────┘
不分代:
┌─────────────────────────────────────┐
│ G1 (JDK9+ 默认) --- 分区 Region │
│ ZGC (JDK11+) --- 亚毫秒级停顿 │
│ Shenandoah (JDK12+) --- 并发回收 │
└─────────────────────────────────────┘
收集器详细对比
| 收集器 | 代 | 线程 | 算法 | 目标 | 适用场景 |
|---|---|---|---|---|---|
| Serial | 新生代 | 单线程 | 复制 | 简单高效 | Client 模式、单核 |
| ParNew | 新生代 | 多线程 | 复制 | 缩短 STW | 配合 CMS |
| Parallel Scavenge | 新生代 | 多线程 | 复制 | 吞吐量优先 | 后台计算任务 |
| Serial Old | 老年代 | 单线程 | 标记-整理 | --- | Client 模式 |
| Parallel Old | 老年代 | 多线程 | 标记-整理 | 吞吐量 | 配合 Parallel Scavenge |
| CMS | 老年代 | 并发 | 标记-清除 | 低停顿 | 互联网 Web 应用 |
| G1 | 全堆 | 并发 | 标记-整理 + 复制 | 可预测停顿 | JDK9+ 默认,大堆 |
| ZGC | 全堆 | 并发 | 染色指针 | 亚毫秒停顿 | 超大堆、低延迟 |
CMS 详细过程(4 个阶段)
1. 初始标记 (STW) --- 标记 GC Roots 直接关联对象,极短
2. 并发标记 --- 从 GC Roots 遍历对象图,与用户线程并发,耗时长
3. 重新标记 (STW) --- 修正并发标记期间变动的对象,比初始标记长但可控
4. 并发清除 --- 清除垃圾对象,与用户线程并发
CMS 缺点:
- 内存碎片(标记-清除算法,可能触发 Full GC)
- 并发阶段占用 CPU 资源
- 浮动垃圾(并发标记期间新产生的垃圾,本次 GC 无法回收)
- Concurrent Mode Failure(老年代在并发阶段被填满,退化到 Serial Old)
G1 核心特点
- 分区思想 :将堆分成多个大小相等的 Region,不再物理划分新生代/老年代。
- Garbage-First:优先回收垃圾最多的 Region,最大化收益。
- 可预测停顿 :可设置期望停顿时间
-XX:MaxGCPauseMillis。 - RSet(Remembered Set):记录跨 Region 引用,避免全堆扫描。
各 JDK 版本默认收集器
| JDK 版本 | 默认收集器 |
|---|---|
| JDK 8 | Parallel Scavenge(新生代) + Parallel Old(老年代) |
| JDK 9 | G1 |
| JDK 11+ | G1(ZGC 作为实验性功能) |
| JDK 17 | 推荐 ZGC(低延迟场景) |
五、类加载机制
5.1 类加载的 5 个阶段
加载 → 验证 → 准备 → 解析 → 初始化
│ │ │ │ │
│ │ │ │ └── 执行静态代码块、静态变量赋代码值
│ │ │ └── 符号引用 → 直接引用(内存地址)
│ │ └── 静态变量分配内存并赋默认值(非代码赋值)
│ └── 校验字节码格式、元数据、字节码、符号引用合法性
└── 读取 class 字节码,生成 Class 对象放入元空间
各阶段详解:
| 阶段 | 做什么 | 关键点 |
|---|---|---|
| 加载 | 通过全限定名获取字节流 → 转为方法区数据结构 → 生成 Class 对象 |
可自定义类加载器实现 |
| 验证 | 文件格式、元数据、字节码、符号引用验证 | 保证 JVM 安全 |
| 准备 | 为静态变量分配内存,设置类型默认值 | static int a = 100; 此阶段 a = 0 |
| 解析 | 符号引用(字面量)转为直接引用(内存地址) | 可在初始化之后(动态绑定) |
| 初始化 | 执行 <clinit>() 方法:静态赋值、静态代码块 |
a 才变成 100 |
5.2 双亲委派模型
核心思想:类加载器收到加载请求 → 先委派给父加载器 → 父加载器无法加载 → 自己才加载。
┌──────────────────────────┐
│ Bootstrap ClassLoader │ ← C++ 实现,加载 rt.jar(String/Object 等核心类)
│ 启动类加载器 │ $JAVA_HOME/jre/lib/
├──────────────────────────┤
│ Extension ClassLoader │ ← 加载 ext 目录下的扩展类
│ 扩展类加载器 (JDK9 废弃) │ $JAVA_HOME/jre/lib/ext/
├──────────────────────────┤
│ Application ClassLoader │ ← 加载 classpath 下用户写的类
│ 应用程序类加载器 │
├──────────────────────────┤
│ 自定义类加载器 │ ← 继承 ClassLoader,实现特殊加载逻辑
└──────────────────────────┘
为什么用双亲委派?
- 避免类的重复加载(父加载器加载过的,子加载器不用重复加载)
- 保护核心类库安全(防止用户自定义
java.lang.String替换核心类)
破坏双亲委派的场景:
- JDBC(SPI 机制):核心类调用实现类,使用线程上下文类加载器
- Tomcat:多个 Web 应用需要隔离各自的类
- OSGi:模块化热部署
六、常见 JVM 调优参数速查
| 参数 | 含义 | 示例 |
|---|---|---|
-Xms |
初始堆大小 | -Xms512m |
-Xmx |
最大堆大小 | -Xmx4g |
-Xmn |
新生代大小 | -Xmn256m |
-Xss |
线程栈大小 | -Xss1m |
-XX:NewRatio |
老年代/新生代比例 | -XX:NewRatio=2(老年代:新生代=2:1) |
-XX:SurvivorRatio |
Eden/Survivor 比例 | -XX:SurvivorRatio=8(Eden:S0=8:1) |
-XX:MaxTenuringThreshold |
晋升老年代年龄阈值 | -XX:MaxTenuringThreshold=15 |
-XX:MetaspaceSize |
元空间初始大小 | -XX:MetaspaceSize=128m |
-XX:MaxMetaspaceSize |
元空间最大大小 | -XX:MaxMetaspaceSize=256m |
-XX:+UseG1GC |
使用 G1 收集器 | --- |
-XX:+UseZGC |
使用 ZGC 收集器 | --- |
-XX:MaxGCPauseMillis |
期望 GC 停顿时间 | -XX:MaxGCPauseMillis=200 |
七、面试高频问题速答
Q1:堆和栈的区别?
| 维度 | 堆 | 栈 |
|---|---|---|
| 存储内容 | 对象实例、数组 | 局部变量、方法调用 |
| 线程共享 | 全局共享 | 线程私有 |
| 内存管理 | GC 自动回收 | 方法结束自动出栈 |
| 异常 | OOM | StackOverflowError / OOM |
| 大小 | 可调(-Xmx) | 较小(-Xss,默认 1M) |
Q2:Minor GC、Major GC、Full GC 区别?
- Minor GC:仅新生代回收,频繁、速度快。
- Major GC:仅老年代回收(CMS 的并发收集算 Major GC)。
- Full GC :整堆 + 元空间回收,STW 长 ,尽量避免。常见触发条件:
System.gc()、老年代空间不足、元空间不足、CMS 并发失败等。
Q3:什么时候对象进入老年代?
- 年龄超过
MaxTenuringThreshold(默认 15) - 大对象直接分配在老年代
- 动态年龄判断:Survivor 中同年龄对象大小总和超过 Survivor 一半,该年龄及以上直接晋升
- 空间分配担保:Minor GC 后 Survivor 放不下,直接进入老年代
Q4:CMS 和 G1 的区别?
| 维度 | CMS | G1 |
|---|---|---|
| 内存布局 | 连续物理分代 | 分区 Region,逻辑分代 |
| 算法 | 标记-清除 | 标记-整理 + 复制 |
| 碎片 | 会产生碎片 | 整理避免碎片 |
| 停顿预测 | 无法预测 | 可设置停顿目标 |
| 适用堆大小 | 中小堆(< 4G-6G) | 大堆(> 4G-6G) |
| 并发阶段 | 不压缩,可能 Full GC | 优先回收收益高的 Region |
Q5:什么是 STW(Stop The World)?
GC 执行时,所有用户线程暂停。不同收集器的 STW 时长不同:
- Serial:全程 STW
- CMS:仅初始标记和重新标记两个短阶段 STW
- G1:标记阶段和部分回收阶段
- ZGC:亚毫秒级,几乎无 STW
Q6:类加载过程,双亲委派为什么这样设计?
- 避免重复加载:父 ClassLoader 加载过的类,子 ClassLoader 不再加载。
- 安全隔离 :防止自定义的
java.lang.String等类篡改核心 API。 - 加载顺序:Bootstrap → Extension → Application → 自定义。
Q7:OOM 常见原因和排查思路?
| 类型 | 原因 | 排查 |
|---|---|---|
Java heap space |
堆内存不足,对象太多 | jmap -dump + MAT/JProfiler 分析 |
GC overhead limit exceeded |
GC 时间 > 98% 但回收 < 2% | 调大堆或优化代码 |
Metaspace |
类加载过多(动态代理、大量 JSP) | -XX:MaxMetaspaceSize 调大 |
unable to create new native thread |
线程创建过多 | ulimit -u,减少线程或调小 -Xss |
排查工具 :jps、jstat、jmap、jstack、jconsole、VisualVM、Arthas(阿里开源)。
📌 记忆口诀:类加内存 GC,堆分两代四区,可达分析定生死,CMS 低停 G1 分区,JDK8 默认 Parallel,JDK9 起用 G1。