JVM调优与原理深度解析:面试必备核心知识点全覆盖
🎯 写在前面:JVM(Java Virtual Machine)是Java程序员必须掌握的核心知识,无论是面试还是工作中排查问题,都离不开对JVM的深入理解。这篇文章将带你从原理到调优,系统掌握JVM的核心知识点!
目录导航
一、JVM整体架构
1.1 架构图一览
┌─────────────────────────────────────────────────────────────────┐
│ JVM 运行时架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ 类加载器 │───▶│ 运行时 │───▶│ 执行引擎 │ │
│ │ ClassLoader │ │ 数据区 │ │ Execution Engine │ │
│ └──────────────┘ │ Runtime │ └──────────────────┘ │
│ │ Data Areas │ │ │
│ └──────────────┘ ▼ │
│ │ ┌──────────┐ │
│ └─────────────▶│ 本地方法 │ │
│ │ 接口 │ │
│ │ Native │ │
│ │ Interface│ │
│ └──────────┘ │
└─────────────────────────────────────────────────────────────────┘
1.2 各组件职责
| 组件 | 职责 |
|---|---|
| 类加载器 | 负责加载.class文件,生成Class对象 |
| 运行时数据区 | 管理内存,分区存放不同类型数据 |
| 执行引擎 | 解释执行字节码,JIT编译优化 |
| 本地方法接口 | 调用Native方法,连接本地库 |
二、类加载机制
2.1 双亲委派模型(必问!)
┌─────────────────────────────────────────────┐
│ 类加载器双亲委派模型 │
├─────────────────────────────────────────────┤
│ │
│ Bootstrap ClassLoader │
│ (启动类加载器 - C++实现) │
│ ▲ │
│ │ 委派 │
│ │ │
│ Extension ClassLoader │
│ (扩展类加载器) │
│ ▲ │
│ │ 委派 │
│ │ │
│ Application ClassLoader │
│ (应用类加载器 - 默认加载器) │
│ ▲ │
│ │ 委派 │
│ │ │
│ 自定义类加载器 (User Defined) │
│ │
└─────────────────────────────────────────────┘
2.2 双亲委派工作流程
加载类请求 → ApplicationClassLoader
↓ 检查是否已加载?
├─ 已加载 → 返回Class对象
└─ 未加载 → 向上委派给父加载器
↓ 检查是否已加载?
├─ 已加载 → 返回Class对象
└─ 未加载 → 继续向上委派
...
↓ 最终
BootstrapClassLoader
↓
无法加载 → 向下尝试
↓
ExtensionClassLoader
↓
ApplicationClassLoader
↓
自己加载 → defineClass
2.3 为什么需要双亲委派?
| 作用 | 说明 |
|---|---|
| 安全机制 | 防止核心API被篡改,如自定义String类不会被加载 |
| 避免重复加载 | 父加载器已加载的类,子加载器不会再次加载 |
| 类的隔离性 | 不同加载器加载的类是不同的类 |
2.4 类加载过程
类加载完整过程:加载 → 验证 → 准备 → 解析 → 初始化
| 阶段 | 详细说明 |
|---|---|
| 加载(Loading) | 读取.class文件,生成字节数组,创建Class对象 |
| 验证(Verification) | 验证字节码格式、语义、安全性 |
| 准备(Preparation) | 为类的静态变量分配内存,初始化默认值 |
| 解析(Resolution) | 将符号引用转换为直接引用 |
| 初始化(Initialization) | 执行静态代码块,给静态变量赋值 |
三、运行时数据区
3.1 内存分区全景图
┌────────────────────────────────────────────────────────────────────┐
│ JVM 运行时数据区 │
├──────────────────────────┬─────────────────────────────────────────┤
│ │ │
│ ┌────────────────┐ │ ┌─────────────────────────────────┐ │
│ │ 方法区 │ │ │ 堆 Heap │ │
│ │ Method Area │ │ │ ┌───────────┬───────────────┐ │ │
│ │ (元空间 Meta) │ │ │ │ 新生代 │ 老年代 │ │ │
│ │ │ │ │ │ ┌────┐┌───┐│ │ │ │
│ │ - 类信息 │ │ │ │ │Eden││S0/S1││ Old Gen │ │ │
│ │ - 静态变量 │ │ │ │ │ ││ ││ │ │ │
│ │ - 常量池 │ │ │ │ └────┘└───┘│ │ │ │
│ │ - JIT代码缓存 │ │ │ │ Young Gen │ │ │ │
│ └────────────────┘ │ │ └───────────┴───────────────┘ │ │
│ │ └─────────────────────────────────┘ │
│ ┌───────────────────┐ │ ┌─────────────────────────────────┐ │
│ │ 虚拟机栈 │ │ │ 本地方法栈 │ │
│ │ JVM Stack │ │ │ Native Method Stack │ │
│ │ │ │ └─────────────────────────────────┘ │
│ │ - 局部变量表 │ │ ┌─────────────────────────────────┐ │
│ │ - 操作数栈 │ │ │ 程序计数器 │ │
│ │ - 动态链接 │ │ │ PC Register │ │
│ │ - 方法返回地址 │ │ └─────────────────────────────────┘ │
│ └───────────────────┘ │ │
│ │ │
└──────────────────────────┴─────────────────────────────────────────┘
3.2 各区域详解
3.2.1 堆(Heap)- 重点!
堆内存结构(默认比例 1:2)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Young Generation (新生代) Old Generation (老年代)
┌─────────────────┐ ┌─────────────────────────┐
│ Eden │ │ │
│ 8/10 │ │ 2/3 │
│ │ │ │
├───────┬─────────┤ │ │
│ S0 │ S1 │ │ │
│ 1/10 │ 1/10 │ │ │
└───────┴─────────┘ └─────────────────────────┘
┌─────────────────────────────────────────────┐
│ 逃逸分析优化 │
│ 标量替换、栈上分配、同步消除 │
└─────────────────────────────────────────────┘
内存分配公式:
总堆内存 = 新生代 + 老年代
新生代 = Eden + S0 + S1 = 1 + 1/10 + 1/10 = 1.2
老年代 = 2
配置参数:-Xms256m -Xmx256m -Xmn128m
→ 新生代128M,老年代128M
配置参数:-XX:NewRatio=2
→ 新生代占1/3,老年代占2/3
3.2.2 虚拟机栈(JVM Stack)
┌─────────────────────────────────────┐
│ 虚拟机栈内存模型 │
├─────────────────────────────────────┤
│ │
│ ┌─────────────────────────────┐ │
│ │ 栈帧 Stack Frame │ │
│ │ ┌───────────────────────┐ │ │
│ │ │ 局部变量表 │ │ │
│ │ │ Local Variables │ │ │
│ │ ├───────────────────────┤ │ │
│ │ │ 操作数栈 │ │ │
│ │ │ Operand Stack │ │ │
│ │ ├───────────────────────┤ │ │
│ │ │ 动态链接 │ │ │
│ │ │ Dynamic Linking │ │ │
│ │ ├───────────────────────┤ │ │
│ │ │ 方法返回地址 │ │ │
│ │ │ Return Address │ │ │
│ │ └───────────────────────┘ │ │
│ └─────────────────────────────┘ │
│ │
│ 线程独享,每个线程一个栈 │
│ 栈大小:-Xss1m (默认1MB) │
│ │
└─────────────────────────────────────┘
3.2.3 方法区(MetaSpace)
| 版本 | 实现 | 参数 |
|---|---|---|
| JDK 7 | PermGen(永久代) | -XX:PermSize=128m |
| JDK 8+ | MetaSpace(元空间) | -XX:MetaspaceSize=256m |
方法区存储内容:
├── 类的元信息(类名、访问修饰符、字段、方法等)
├── 运行时常量池(字面量、符号引用)
├── 静态变量(JDK 7还在堆,JDK 8移至元空间)
├── JIT编译后的代码缓存
└── 虚拟机已加载的类信息
3.3 常见内存溢出(OOM)
| 区域 | 原因 | 解决方案 |
|---|---|---|
| 堆溢出 | 对象过多无法回收 | -Xmx 调大,检查内存泄漏 |
| 栈溢出 | 递归过深、线程过多 | -Xss 调大,减少递归 |
| 元空间溢出 | 类过多(动态代理、CGlib) | -XX:MetaspaceSize 调大 |
| 直接内存溢出 | NIO使用过多 | -XX:MaxDirectMemorySize |
四、垃圾回收算法
4.1 判断对象可回收
4.1.1 引用计数法(不用!)
原理:对象被引用+1,引用失效-1,为0则回收
┌─────────┐ ┌─────────┐
│对象 A │──────▶│对象 B │ A引用B
│计数=1 │ │计数=2 │ A和C都引用B
└─────────┘ └─────────┘
▲
│
┌─────────┐
│对象 C │──────▶│对象 B │ C引用B
│计数=0 │ │计数=2 │
└─────────┘ └─────────┘
问题:循环引用无法回收 ❌
4.1.2 可达性分析(GC Roots)- 重点!
可作为GC Roots的对象:
┌─────────────────────────────────────┐
│ ✓ 虚拟机栈(栈帧中的本地变量表) │
│ ✓ 方法区中静态属性引用的对象 │
│ ✓ 方法区中常量引用的对象 │
│ ✓ 本地方法栈中JNI引用的对象 │
│ ✓ 虚拟机内部引用(Class对象等) │
│ ✓ 同步锁(synchronized)持有的对象 │
│ ✓ JMXBean、Callback对象等 │
└─────────────────────────────────────┘
可达性分析示例:
┌─────────────────────────────────────┐
│ GC Root │
│ ★ │
└──────────────┬──────────────────────┘
│
┌────────▼────────┐
│ 对象 A │
└────────┬────────┘
│
┌──────────────┼──────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ 对象 B │ │ 对象 C │ │ 对象 D │──▶ ...
└────┬────┘ └─────────┘ └─────────┘
│ ▲
│ │
▼ │
┌─────────┐ │
│ 对象 E │─────────┘ ← 可达,通过C引用
└─────────┘
┌─────────┐ ┌─────────┐ ← 不可达,循环引用
│ 对象 F │◀───│ 对象 G │
└─────────┘ └─────────┘ 等待回收 ✓
4.2 标记-清除算法(Mark-Sweep)
原理:
┌─────────────────────────────────────────────┐
│ 标记阶段:遍历所有对象,标记存活对象 │
│ 清除阶段:统一回收未标记对象 │
└─────────────────────────────────────────────┘
内存示意图:
┌─────────────────────────────────────────────┐
│ 清除前: │
│ [●][●][○][●][○][○][●][○][●][○][●][●] │
│ ↑ ↑ ↑ ↑ ↑ ↑ │
│ 存活 死亡存活死亡死亡存活死亡存活... │
│ │
│ 清除后: │
│ [●][●][ ][●][ ][ ][●][ ][●][ ][●][●] │
│ │
│ ↑ │
│ 产生内存碎片 │
└─────────────────────────────────────────────┘
缺点:❌ 产生内存碎片
4.3 复制算法(Copying)
原理:将内存分成两块,每次只使用一块,回收时复制存活对象到另一块
┌─────────────────────────────────────────────┐
│ 内存划分: │
│ │
│ ┌─────────────────┬─────────────────┐ │
│ │ From Space │ To Space │ │
│ │ 50% │ 50% │ │
│ └─────────────────┴─────────────────┘ │
│ │
│ 使用中 空闲 │
└─────────────────────────────────────────────┘
回收过程:
┌─────────────────────────────────────────────┐
│ │
│ From Space To Space │
│ ┌─────┬─────┬─────┐ ┌─────────────┐ │
│ │ A │ B │ │ ───▶ │ A B │ │
│ │存活 │存活 │ 死亡 │ │ 复制过来 │ │
│ └─────┴─────┴─────┘ └─────────────┘ │
│ │
│ 优点:无碎片,空间浪费一半 │
└─────────────────────────────────────────────┘
4.4 标记-整理算法(Mark-Compact)
原理:标记 → 整理(移动存活对象)→ 清除
┌─────────────────────────────────────────────┐
│ 整理前: │
│ [●][○][●][●][○][○][●][○][●][○] │
│ │
│ 标记存活对象: │
│ [★][○][★][★][○][○][★][○][★][○] │
│ │
│ 整理后(向一端移动): │
│ [★][★][★][★][★][○][○][○][○][○] │
│ ←存活→←──────── 空闲 ────────→ │
│ │
│ 优点:✓ 无碎片,适合老年代 │
│ 缺点:需要移动对象,开销大 │
└─────────────────────────────────────────────┘
4.5 分代收集算法(重点!)
┌──────────────────────────────────────────────────────┐
│ 分代收集策略 │
├──────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ 新生代 │ │ 老年代 │ │
│ │ (Young Gen) │ │ (Old/Tenured Gen) │ │
│ │ │ │ │ │
│ │ ┌────┬────┬────┐ │ │ │ │
│ │ │Eden│ S0 │ S1 │ │ │ │ │
│ │ │ 8/10│1/10│1/10│ │ │ 3/4 │ │
│ │ └────┴────┴────┘ │ │ │ │
│ │ │ │ │ │
│ │ Minor GC │ │ Major/Full GC │ │
│ │ 频繁/快速 │ │ 慢/STOP THE WORLD │ │
│ └────────┬─────────┘ └───────────┬───────────┘ │
│ │ │ │
│ │ 晋升 │ │
│ ▼ (15次GC后) │ │
│ └─────────────────────────┘ │
│ │
│ 回收算法: 回收算法: │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ 复制算法 │ │ 标记-整理算法 │ │
│ │ (高效/无碎片) │ │ (无碎片/慢) │ │
│ └────────────────┘ └────────────────┘ │
│ │
└──────────────────────────────────────────────────────┘
对象分配流程:
┌─────────────────────────────────────────────────────┐
│ │
│ 新对象创建 ──▶ Eden区是否有足够空间? │
│ │ │
│ ┌─────────┴─────────┐ │
│ │是 │否 │
│ ▼ ▼ │
│ 分配到Eden区 Minor GC │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ S0 或 S1 存活对象 存活对象 │
│ (复制算法) 转移到另一块 晋升老年代 │
│ │
└─────────────────────────────────────────────────────┘
五、垃圾收集器
5.1 收集器家族图谱
┌─────────────────────────────────────────────────────────────────────┐
│ 垃圾收集器家族 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 新生代收集器 老年代收集器 │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Serial GC │ │ Serial Old │ │
│ │ (单线程) │ │ (单线程) │ │
│ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │
│ │ 组合 │ 组合 │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ ParNew GC │ │ CMS GC │ │
│ │ (并行) │ ────────────▶ │ (并发) │ │
│ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │
│ │ 组合 │ 组合 │
│ ▼ │ │
│ ┌─────────────┐ │ │
│ │ Parallel │ │ │
│ │ Scavenge │ │ │
│ │ (吞吐量优先)│ │ │
│ └──────┬──────┘ │ │
│ │ ▼ │
│ │ ┌─────────────────┐ │
│ │ │ G1 GC │ │
│ │ │ (_REGION_分代) │ │
│ │ └────────┬────────┘ │
│ │ │ │
│ │ │ JDK 11+ │
│ │ ▼ │
│ │ ┌─────────────────┐ │
│ │ │ ZGC │ │
│ │ │ (低延迟 <1ms) │ │
│ │ └────────┬────────┘ │
│ │ │ │
│ │ │ JDK 15+ │
│ │ ▼ │
│ │ ┌─────────────────┐ │
│ │ │ Shenandoah │ │
│ │ │ (低延迟) │ │
│ │ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
5.2 各收集器对比
| 收集器 | 线程 | 算法 | 特点 | 适用场景 |
|---|---|---|---|---|
| Serial | 单线程 | 复制 | 简单高效 | Client模式、堆小 |
| ParNew | 多线程 | 复制 | Serial多核版 | Server多核首选 |
| Parallel Scavenge | 多线程 | 复制 | 吞吐量优先 | 后台批处理 |
| Serial Old | 单线程 | 标记-整理 | 老年代 | Client模式 |
| Parallel Old | 多线程 | 标记-整理 | 吞吐量优先 | 组合Parallel |
| CMS | 并发 | 标记-清除 | 低延迟 | 响应优先应用 |
| G1 | 并发 | 标记-整理 | 可预测延迟 | 大堆应用 |
| ZGC | 并发 | 标记-整理 | <1ms延迟 | 超大堆TB级 |
| Shenandoah | 并发 | 标记-整理 | 低延迟 | RedHat |
5.3 CMS收集器(重点!)
CMS工作流程(并发标记清理):
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ 初始标记 ──▶ 并发标记 ──▶ 重新标记 ──▶ 并发清理 │
│ (STW) (并发) (STW) (并发) │
│ │
│ ━━━━━━━━━━━━ STOP THE WORLD ━━━━━━━━━━━━ │
│ ━━━━━━━━━━━━━━━━━━━ 并发阶段 ━━━━━━━━━━━━━━━━━━━ │
│ │
└─────────────────────────────────────────────────────────────────────┘
详细阶段:
┌─────────────────────────────────────────────────────────────────────┐
│ 阶段1:初始标记 (Initial Mark) - STW │
│ ├─ 标记GC Roots直接引用的对象 │
│ ├─ 速度:快 │
│ └─ 停顿时间:短 │
│ │
│ 阶段2:并发标记 (Concurrent Mark) │
│ ├─ 从GC Roots向下追踪,标记所有存活对象 │
│ ├─ 速度:慢,与应用并发运行 │
│ └─ 特点:可能漏标(remark解决) │
│ │
│ 阶段3:重新标记 (Remark) - STW │
│ ├─ 修正并发标记期间漏标的对象 │
│ ├─ 停顿时间:比初始标记长 │
│ └─ 多线程并行 │
│ │
│ 阶段4:并发清理 (Concurrent Sweep) │
│ ├─ 清理未标记的垃圾对象 │
│ └─ 特点:与用户线程并发,不STW │
└─────────────────────────────────────────────────────────────────────┘
CMS缺点:
┌─────────────────────────────────────┐
│ ❌ 产生内存碎片 │
│ ❌ 与用户线程争用CPU资源 │
│ ❌ 无法处理浮动垃圾 │
│ ❌ Concurrent Mode Failure │
│ (并发失败,启用Serial Old) │
└─────────────────────────────────────┘
5.4 G1收集器(重点!JDK 11+默认)
G1(Garbage First)核心思想:
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ 将堆划分为多个大小相等的Region(1MB-32MB) │
│ │
│ ┌─────────┬─────────┬─────────┬─────────┐ │
│ │ Eden │ Survivor│ Old │ Hum │ ← H Humongous大对象 │
│ │ Region │ Region │ Region │ Region │ │
│ └─────────┴─────────┴─────────┴─────────┘ │
│ ┌─────────┬─────────┬─────────┬─────────┐ │
│ │ Eden │ Survivor│ Old │ Hum │ │
│ └─────────┴─────────┴─────────┴─────────┘ │
│ ┌─────────┬─────────┬─────────┬─────────┐ │
│ │ Eden │ Survivor│ Old │ Free │ │
│ └─────────┴─────────┴─────────┴─────────┘ │
│ │
│ 回收时:优先回收垃圾最多的Region │
│ │
└─────────────────────────────────────────────────────────────────────┘
G1工作流程:
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ Young GC ──▶ Mix GC ──▶ Old Generation │
│ │
│ Young GC: │
│ ├─ 回收所有年轻代Region │
│ ├─ Eden + Survivor → 新Survivor + 部分Old │
│ └─ STW,但可预测 │
│ │
│ Mix GC: │
│ ├─ 回收所有年轻代 + 部分老年代 │
│ ├─ 计算各Region回收价值(垃圾量/回收成本) │
│ ├─ 优先回收高价值Region │
│ └─ 可通过 -XX:MaxGCPauseMillis 控制停顿时间 │
│ │
└─────────────────────────────────────────────────────────────────────┘
G1关键参数:
┌─────────────────────────────────────┐
│ -XX:+UseG1GC 启用G1 │
│ -XX:MaxGCPauseMillis=200 最大停顿│
│ -XX:G1HeapRegionSize=1m Region大小│
│ -XX:InitiatingHeapOccupancyPercent=45│
│ 老年代阈值│
└─────────────────────────────────────┘
六、JVM调优实战
6.1 常见调优场景
| 场景 | 表现 | 解决方案 |
|---|---|---|
| Young GC频繁 | Eden区太小,对象分配频繁 | -Xmn 调大,-XX:SurvivorRatio 调整 |
| Full GC频繁 | 老年代满了或内存碎片 | -Xmx/-Xms 调大,使用G1 |
| OOM | OutOfMemoryError | 分析dump文件,定位泄漏 |
| GC时间过长 | Stop The World过长 | 选择低延迟收集器G1/ZGC |
| CPU高 | GC线程占用资源 | 减少GC频率,调优参数 |
6.2 调优参数汇总
堆内存配置:
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ -Xms256m 初始堆大小(等价于 -XX:InitialHeapSize=256m) │
│ -Xmx256m 最大堆大小(等价于 -XX:MaxHeapSize=256m) │
│ -Xmn128m 新生代大小(等价于 -XX:NewSize=128m) │
│ -Xss1m 栈大小(等价于 -XX:ThreadStackSize=1m) │
│ │
│ -XX:NewRatio=2 新生代:老年代 = 1:2 │
│ -XX:SurvivorRatio=8 Eden:Survivor = 8:1 │
│ │
│ 示例计算: │
│ -Xmx256m -Xmn128m │
│ → 新生代128M = 80M(Eden) + 16M(S0) + 16M(S1) │
│ → 老年代128M │
│ │
└─────────────────────────────────────────────────────────────────────┘
元空间配置:
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ JDK 7及之前: │
│ -XX:PermSize=128m 永久代初始大小 │
│ -XX:MaxPermSize=256m 永久代最大大小 │
│ │
│ JDK 8+: │
│ -XX:MetaspaceSize=256m 元空间初始大小 │
│ -XX:MaxMetaspaceSize=512m 元空间最大大小 │
│ │
└─────────────────────────────────────────────────────────────────────┘
GC日志配置:
┌─────────────────────────────────────────────────────────────────────┐
│ │
│ -XX:+PrintGC 打印GC日志 │
│ -XX:+PrintGCDetails 详细GC日志 │
│ -XX:+PrintGCDateStamps 打印日期时间戳 │
│ -XX:+PrintHeapAtGC GC时打印堆信息 │
│ -Xloggc:/path/to/gc.log GC日志文件路径 │
│ │
└─────────────────────────────────────────────────────────────────────┘
6.3 内存泄漏排查流程
┌─────────────────────────────────────────────────────────────────────┐
│ OOM问题排查步骤 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 添加JVM参数: │
│ -XX:+HeapDumpOnOutOfMemoryError │
│ -XX:HeapDumpPath=/path/to/dump.hprof │
│ │
│ 2. 生成堆转储文件(.hprof) │
│ │
│ 3. 使用分析工具: │
│ ├─ MAT (Memory Analyzer Tool) - 最常用 │
│ ├─ VisualVM - JDK自带 │
│ ├─ Arthas - 阿里诊断工具 │
│ └─ JProfiler - 商业工具 │
│ │
│ 4. 分析步骤: │
│ ├─ 查看dominator tree(内存占用TOP对象) │
│ ├─ 查看泄漏嫌疑(Retained Heap最大) │
│ ├─ 追溯引用链(GC Roots → 泄漏对象) │
│ └─ 定位问题代码 │
│ │
└─────────────────────────────────────────────────────────────────────┘
6.4 调优案例
案例1:MySQL连接池导致的OOM
问题现象:
├─ Heap Dump显示大量com.mysql.cj.jdbc.ConnectionImpl对象
├─ 每个连接占用约200KB,1000个连接 = 200MB
└─ 原因:未配置连接池最大连接数或回收策略
解决方案:
├─ 配置Druid/HikariCP最大连接数
├─ 配置连接超时和空闲回收
└─ -Xmx 调大 + 代码修复
案例2:缓存导致的OOM
问题现象:
├─ 使用HashMap做本地缓存
├─ 无上限put导致内存持续增长
└─ Full GC后内存不降
解决方案:
├─ 使用WeakHashMap或Guava Cache
├─ 配置缓存大小上限
├─ 设置TTL过期时间
└─ 改用Redis分布式缓存
七、常见面试题
7.1 JVM基础
Q1:什么是JVM?JDK、JRE、JVM的关系?
┌─────────────────────────────────────┐
│ JDK > JRE > JVM │
├─────────────────────────────────────┤
│ │
│ JDK (Java Development Kit) │
│ ├─ 包含JRE │
│ ├─ 包含编译器 javac │
│ ├─ 包含调试工具 │
│ └─ 开发人员使用 │
│ │ │
│ ▼ │
│ JRE (Java Runtime Environment) │
│ ├─ 包含JVM │
│ ├─ 包含类库 │
│ └─ 运行人员使用 │
│ │ │
│ ▼ │
│ JVM (Java Virtual Machine) │
│ ├─ 字节码解释/执行 │
│ └─ 内存管理 │
│ │
└─────────────────────────────────────┘
Q2:Java代码执行流程?
源码.java
↓ 编译
字节码.class
↓ 类加载
JVM内存
↓ 解释/JIT编译
机器码
↓ 执行
程序运行
7.2 类加载
Q3:什么是双亲委派模型?为什么要用?
答案要点:
1. 加载类时,先向上委派给父加载器处理
2. 父加载器无法加载时,才由自己加载
3. 目的:
- 避免类的重复加载
- 保护Java核心API不被篡改
- 保证类加载的安全性
Q4:类加载的过程?
加载 → 验证 → 准备 → 解析 → 初始化
各阶段详解:
- 加载:读取字节码,生成Class对象
- 验证:验证字节码安全性
- 准备:分配内存,初始化默认值
- 解析:符号引用→直接引用
- 初始化:执行静态代码,赋初始值
7.3 内存与GC
Q5:JVM内存结构?
┌──────────────────────────────────────────────────────────┐
│ JVM 运行时数据区 │
├──────────────────────────┬───────────────────────────────┤
│ 线程共享: │ 线程私有: │
│ ├─ 堆 (Heap) │ ├─ 虚拟机栈 (Stack) │
│ └─ 元数据区 (MetaSpace) │ ├─ 本地方法栈 (Native) │
│ │ └─ 程序计数器 (PC) │
└──────────────────────────┴───────────────────────────────┘
Q6:什么是Minor GC和Full GC?区别?
| 类型 | 触发条件 | 回收区域 | 停顿时间 |
|---|---|---|---|
| Minor GC | Eden区满 | 新生代 | 短,通常<100ms |
| Full GC | 老年代满/显式调用 | 全堆+元空间 | 长,通常>500ms |
Q7:GC算法有哪些?优缺点?
| 算法 | 优点 | 缺点 | 适用 |
|---|---|---|---|
| 标记-清除 | 简单 | 内存碎片 | 老年代 |
| 复制 | 无碎片,高效 | 浪费一半空间 | 新生代 |
| 标记-整理 | 无碎片 | 移动对象,慢 | 老年代 |
| 分代收集 | 综合最优 | 参数调优复杂 | 通用 |
Q8:线上Full GC频繁怎么排查?
排查步骤:
1. 添加GC参数,打印详细日志
2. 分析GC日志,确定GC类型和频率
3. dump堆内存,分析对象分布
4. 定位泄漏对象,追溯代码
5. 修复代码 + 调整JVM参数
7.4 调优实战
Q9:如何设置JVM参数?给一个8G内存服务器的配置
通用配置(8G服务器,4G给Java):
┌─────────────────────────────────────────────┐
│ │
│ -Xms4g -Xmx4g 堆大小 │
│ -Xmn2g 新生代 │
│ -XX:MetaspaceSize=256m 元空间 │
│ -XX:+UseG1GC 使用G1收集器 │
│ -XX:MaxGCPauseMillis=200 最大停顿200ms │
│ -XX:+HeapDumpOnOutOfMemoryError │
│ -XX:HeapDumpPath=/var/log/java/dump.hprof │
│ -Xloggc:/var/log/java/gc.log │
│ │
│ 内存计算: │
│ 新生代 2G = 1.6G(Eden) + 0.2G(S0) + 0.2G(S1)│
│ 老年代 2G │
│ │
└─────────────────────────────────────────────┘
Q10:什么是内存泄漏和内存溢出?
┌─────────────────────────────────────────────────────────┐
│ │
│ 内存泄漏 (Memory Leak) │
│ ├─ 定义:对象无法被GC回收,但已不再使用 │
│ ├─ 原因:长生命周期对象持有短生命周期对象引用 │
│ ├─ 后果:内存逐渐耗尽,最终OOM │
│ └─ 示例:静态集合、监听器、未关闭的资源 │
│ │
│ 内存溢出 (Memory Overflow) │
│ ├─ 定义:内存不够用,无法分配所需内存 │
│ ├─ 原因:内存泄漏 / 对象过大 / 内存配置过小 │
│ └─ 表现:OutOfMemoryError │
│ │
└─────────────────────────────────────────────────────────┘
总结
┌─────────────────────────────────────────────────────────────────────┐
│ JVM知识体系总结 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 类加载 │ │ 内存管理 │ │ 垃圾回收 │ │
│ │ │ │ │ │ │ │
│ │ • 双亲委派 │ │ • 运行时数据区 │ │ • GC算法 │ │
│ │ • 加载过程 │ │ • 堆/栈/方法区 │ │ • 收集器 │ │
│ │ • 类加载器 │ │ • 内存分配 │ │ • 分代回收 │ │
│ │ │ │ │ │ │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │ │
│ └───────────────────────┼───────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ JVM调优与排查 │ │
│ │ │ │
│ │ • 参数配置 │ │
│ │ • OOM排查 │ │
│ │ • 性能监控 │ │
│ └─────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
写在最后
🎯 核心记忆点:
- 双亲委派模型 = 安全 + 防重复加载
- 新生代用复制算法,老年代用标记整理
- G1是JDK 11+默认,低延迟首选
- 调优原则:Minor GC频繁调大新生代,Full GC频繁选G1或ZGC
📢 讨论话题:大家在工作中遇到过哪些JVM问题?是如何排查和解决的?评论区见!
👇 往期推荐:
👍 点赞 + 关注,我们下期再见!