本文写于 2015 年 12 月——JDK 8 普及一年,JDK 9 即将发布(2017-09),G1 正式成为默认 GC。
一、JVM 运行时数据区
1.1 整体架构
| |
1.2 各区域作用
| 区域 | 作用 | 线程 | 异常 |
|---|---|---|---|
| PC 寄存器 | 当前线程执行的字节码行号 | 私有 | 无 |
| VM 栈 | Java 方法调用栈(栈帧:局部变量表 + 操作数栈 + 动态链接 + 方法出口) | 私有 | StackOverflowError / OOM |
| 本地方法栈 | Native 方法调用栈 | 私有 | StackOverflowError / OOM |
| 堆 | 对象实例 + 数组 | 共享 | OOM |
| 方法区 | 类信息 + 常量 + 静态变量 + JIT 代码 | 共享 | OOM(PermGen / Metaspace) |
| 运行时常量池 | 字面量 + 符号引用 | 共享 | OOM |
| 直接内存 | NIO DirectByteBuffer | 共享 | OOM |
1.3 面试高频追问
Q1:堆和栈的区别?
| 维度 | 堆 | 栈 |
|---|---|---|
| 存储 | 对象实例 | 局部变量 + 方法调用 |
| 线程 | 共享 | 私有 |
| GC | 有 | 无 |
| 异常 | OOM | StackOverflow |
| 速度 | 慢 | 快 |
Q2:方法区在 JDK 8 的变化?
- JDK 7:方法区 = PermGen(永久代)
- JDK 8+:方法区 = Metaspace(元空间,使用本地内存)
- 字符串常量池:JDK 7 移出 PermGen,JDK 8 仍在堆
二、堆内存分代
2.1 分代模型
| |
2.2 对象生命周期
关键参数:
| |
2.3 大对象直接进入老年代
避免大对象在 Eden 和 Survivor 之间频繁复制。
| |
三、GC 算法
3.1 三大经典算法
| 算法 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 标记-清除 | 标记存活对象,清除未标记 | 简单 | 内存碎片 |
| 标记-整理 | 标记存活对象,移到一端 | 无碎片 | 移动成本 |
| 复制 | 分两区,存活对象复制到另一区 | 无碎片 + 高效 | 内存利用率 50% |
3.2 分代 GC 组合
| 分代 | 算法 | 原因 |
|---|---|---|
| 年轻代 | 复制 | 对象 90% 死亡,复制量少 |
| 老年代 | 标记-整理 | 存活率高,无内存碎片 |
3.3 主流垃圾收集器
| 收集器 | 算法 | 目标 | 适用 |
|---|---|---|---|
| Serial | 复制 | 客户端 | 单核 + 小堆 |
| ParNew | 复制 | 多核 | 配合 CMS |
| Parallel Scavenge | 复制 | 吞吐量优先 | 后台计算 |
| CMS | 标记-清除 | 停顿时间 | 互联网应用(已废弃) |
| G1 | 分区 + 标记-整理 | 可预测停顿 | JDK 9+ 默认 |
| ZGC | 读屏障 + 染色指针 | 亚毫秒停顿 | JDK 11+ 实验,JDK 15+ 生产 |
| Shenandoah | 读屏障 + 并发整理 | 亚毫秒停顿 | OpenJDK |
3.4 G1 GC 详解
设计目标:可预测的停顿时间(默认 200ms)
核心思想:
- 堆划分为多个 Region(1MB-32MB)
- 每个 Region 独立回收
- 优先回收价值最大的 Region(回收性价比)
关键参数:
| |
回收阶段:
- 初始标记(STW):标记 GC Roots
- 并发标记:遍历对象图
- 最终标记(STW):处理 SATB 记录
- 筛选回收(STW):选择 Region 回收集
3.5 ZGC 详解
设计目标:亚毫秒级停顿(< 10ms),支持 TB 级堆
核心技术:
- 读屏障:并发转移对象
- 染色指针:64 位指针中存储元数据
- 内存多重映射:将多个虚拟地址映射到同一物理地址
关键参数:
| |
四、类加载机制
4.1 类加载 5 阶段
| |
| 阶段 | 动作 |
|---|---|
| 加载 | 读字节码到内存,生成 Class 对象 |
| 验证 | 文件格式、元数据、字节码、符号引用验证 |
| 准备 | 静态变量分配内存 + 默认值(public static int v = 1 → 准备阶段后 v = 0) |
| 解析 | 符号引用替换为直接引用 |
| 初始化 | 执行 <clinit> 静态代码块 + 静态变量赋值 |
4.2 类加载器
四种类加载器:
| 加载器 | 加载路径 | 父加载器 |
|---|---|---|
| Bootstrap | <JAVA_HOME>/lib/rt.jar | 无 |
| Extension | <JAVA_HOME>/lib/ext/*.jar | Bootstrap |
| Application | classpath | Extension |
| Custom | 自定义路径 | Application |
4.3 双亲委派模型
| |
为什么需要双亲委派:
- 避免类重复加载
- 保证 JDK 核心类的安全(防止伪造
java.lang.String)
破坏双亲委派的场景:
- Tomcat WebAppClassLoader(不同 WebApp 隔离)
- OSGi 模块化
- JDBC SPI 加载(线程上下文类加载器)
五、OOM 排查
5.1 常见 OOM 类型
| OOM 类型 | 原因 | 解决 |
|---|---|---|
| Java heap space | 内存泄漏 / 堆太小 | 加内存 / 排查泄漏 |
| GC overhead limit exceeded | 98% 时间 GC,回收 < 2% | 加内存 / 优化代码 |
| PermGen space | 类太多 / 静态变量 | 升级 JDK 8+ 用 Metaspace |
| Metaspace | 动态生成类过多 | 调 -XX:MaxMetaspaceSize |
| Direct buffer memory | NIO DirectByteBuffer | -XX:MaxDirectMemorySize |
| unable to create new native thread | 线程过多 | 减少线程 / 调 -Xss |
5.2 排查步骤
1. 生成堆转储:
| |
2. 分析工具:
- Eclipse MAT:分析内存泄漏(dominator tree / leak suspects)
- VisualVM:实时监控堆 / 线程 / GC
- JProfiler:商业级分析工具
- async-profiler:火焰图 + 内存分配
5.3 实战案例:内存泄漏
现象:应用运行 3 天后 OOM,重启后恢复
排查:
| |
MAT 分析:
- Open Heap Dump → Leak Suspects Report
- Dominator Tree → 找到占用最大的对象
- GC Roots → 找到谁在持有
常见内存泄漏:
- ThreadLocal 没 remove
- 静态集合不断 add
- 未关闭的连接(数据库 / HTTP / Socket)
- 监听器没解注册
六、JVM 调优实战
6.1 调优步骤
6.2 推荐配置(2020+ 生产环境)
| |
6.3 调优目标
| 指标 | 目标 |
|---|---|
| GC 频率 | Young GC 每分钟 < 5 次,Full GC 0 次 |
| GC 停顿 | Young GC < 50ms,Full GC < 200ms |
| 吞吐量 | > 95% |
| 延迟 P99 | < 200ms |
七、写在最后
JVM 面试要点:
- 基础:运行时数据区、堆分代、GC 算法
- 进阶:G1 / ZGC 原理、类加载机制、OOM 排查
- 实战:JVM 参数调优、MAT 分析 dump 文件
- 前沿:ZGC / Shenandoah / GraalVM
