外观
JVM 速查卡
🎯 覆盖 23 题 | ⭐ 高频 8 题 | 预计扫描 10 分钟 📌 先看⭐一句话答案 → 展开要点 → 自测清单检验
一、内存结构
知识地图:堆(对象) + 方法区/Metaspace(类信息) = 共享 | 栈+本地方法栈+PC = 私有
⭐ JVM 内存结构
一句话: JVM 运行时数据区分为线程共享(堆、方法区)和线程私有(虚拟机栈、本地方法栈、程序计数器)五大区域。
线程共享:堆(Eden|S0|S1|老年代) ← GC 主战场,new 的对象都在这
方法区/Metaspace(类信息|常量池|静态变量) ← JDK8 移到本地内存
线程私有:虚拟机栈(栈帧=局部变量表+操作数栈)
本地方法栈(native 方法)
程序计数器 ← 唯一不会 OOM 的区域堆的分代: 年轻代(1/3) = Eden(8) + S0(1) + S1(1) | 老年代(2/3)
晋升老年代的条件:
- 年龄达 15(MaxTenuringThreshold)
- 动态年龄判断(同龄对象总大小 > Survivor 50%)
- 大对象直接进老年代
- Survivor 放不下(空间担保)
⚠️ 易错:JDK 8 方法区改为 Metaspace,使用本地内存(不受 -Xmx 限制)
⭐ OOM 排查
一句话: 配置 -XX:+HeapDumpOnOutOfMemoryError 自动 dump → MAT 分析 Retained Heap → 定位泄漏对象的 GC Root 引用链。
| 区域 | OOM 错误 | 常见原因 |
|---|---|---|
| 堆 | Java heap space | 内存泄漏/对象过多/堆太小 |
| Metaspace | Metaspace | 动态生成大量类(CGLIB) |
| 栈 | StackOverflowError | 无限递归 |
| 程序计数器 | 不会 OOM | — |
CPU 100% 排查四步法: top(找进程) → top -Hp(找线程) → 转16进制 → jstack(搜 nid 定位代码)
二、垃圾回收
知识地图:标记清除(碎片) / 标记复制(空间浪费) / 标记整理(移动慢) → CMS vs G1 vs ZGC
⭐ GC 算法三兄弟
一句话: 三种基础算法——标记清除(有碎片)、标记复制(浪费空间但无碎片,年轻代用)、标记整理(无碎片但移动对象慢,老年代用)。
| 算法 | 优点 | 缺点 | 用在 |
|---|---|---|---|
| 标记-清除 | 简单 | 内存碎片 | CMS 老年代 |
| 标记-复制 | 无碎片,分配快 | 空间利用率50%(改进版Eden:S0:S1=8:1:1) | 年轻代 |
| 标记-整理 | 无碎片,利用率高 | 移动对象要更新引用(STW长) | G1 老年代 |
⭐ GC 触发条件 + Full GC 表现
一句话: Minor GC = Eden 满;Full GC = 老年代不足/担保失败/Metaspace不足/System.gc(),期间 STW 导致接口超时。
Full GC 五大触发条件: 老年代空间不足 | System.gc() | 空间担保失败 | Metaspace 不足 | CMS Concurrent Mode Failure
减少 Full GC: 减少大对象 + 增大年轻代 + 升级 G1/ZGC + 分析 GC 日志找泄漏
⭐ CMS vs G1
一句话: CMS 追求最短 STW(标记清除,有碎片),G1 追求可预测停顿(Region 化 + 标记整理,无碎片);JDK 9+ 默认 G1。
| 维度 | CMS | G1 |
|---|---|---|
| 算法 | 标记-清除(有碎片) | 整体标记-整理(无碎片) |
| 作用范围 | 仅老年代 | 整个堆(Region) |
| 停顿控制 | 不可预测 | -XX:MaxGCPauseMillis(默认200ms) |
| 适合 | 堆 < 6GB | 堆 ≥ 6GB |
| 版本建议 | JDK ≤ 8 | JDK ≥ 9(默认) |
CMS 四阶段: 初始标记(STW) → 并发标记 → 重新标记(STW) → 并发清除
G1 核心: 堆切为等大 Region(1-32MB),每个 Region 动态扮演 Eden/Survivor/Old/Humongous;优先回收垃圾最多的 Region(Garbage First)
ZGC: 停顿 < 10ms 且与堆大小无关;核心技术 = 染色指针 + 读屏障
⚠️ 易错:CMS 的致命缺陷是碎片积累导致 Concurrent Mode Failure,退化为 Serial Old 全停顿
三、类加载
⭐ 类加载五阶段 + 双亲委派
一句话: 加载 → 验证 → 准备(static 赋零值) → 解析(符号→直接引用) → 初始化(执行 clinit);双亲委派从子到父委托加载,保证核心类不被篡改。
Bootstrap(rt.jar) ← Extension(lib/ext) ← Application(classpath) ← 自定义
收到请求 → 先委托父加载器 → 父找不到 → 子自己加载打破双亲委派: ① 自定义 ClassLoader 重写 loadClass()(Tomcat 热部署) ② SPI 线程上下文类加载器(JDBC) ③ OSGi 网状加载
⚠️ 易错:准备阶段
static int x = 10赋零值 0,初始化阶段才赋真实值 10;但static final在准备阶段就赋真实值
补充速览
| 关键词 | 核心答案 |
|---|---|
| 判断垃圾 | 可达性分析(从 GC Root 出发);引用计数有循环引用问题,Java 不用 |
| GC Root | 栈帧局部变量、static 变量、JNI 引用、活跃线程、synchronized 持有对象 |
| 内存泄漏 vs 溢出 | 泄漏 = 对象不再用但 GC 无法回收(仍被引用);泄漏积累导致溢出 |
| 常见泄漏 | 静态 Map 只进不出 / ThreadLocal 未 remove / 未关闭连接 |
| new 对象流程 | 类检查加载 → 分配内存(指针碰撞/空闲列表+TLAB) → 零值初始化 → 设对象头(Mark Word+Klass) → 执行构造 |
| 虚拟线程(JVM角度) | JVM 调度的轻量线程,M:N 映射 OS 线程;IO 阻塞自动卸载 carrier thread |
| ⭐ JIT 编译 | 热点代码(方法调用>10000次)编译为机器码;C1(快编译,轻优化)→C2(慢编译,深优化)分层编译 |
| 逆优化 | JIT 激进优化的假设被打破(如新类加载)→退回解释执行→重新编译 |
| ⭐ 逃逸分析 | 分析对象是否逃出方法/线程;不逃逸→标量替换(拆散为局部变量)/锁消除 |
| 对象不一定在堆上 | HotSpot 通过标量替换间接实现"栈上分配"效果;-XX:+DoEscapeAnalysis 默认开启 |
🧠 助记汇总
| 口诀 | 含义 |
|---|---|
| 加验准解初 | 类加载五阶段 |
| 清复整 | GC 三算法:标记清除/标记复制/标记整理 |
| 栈类JNI线锁 | GC Root 五种来源 |
| E区满Minor,老区满Full | GC 触发条件简记 |
✅ 自测清单
| # | 问题 | 你能说出... |
|---|---|---|
| 1 | JVM 内存结构 | 五大区域 + 各存什么 + 哪些共享哪些私有 |
| 2 | 堆分代 | 比例 + 晋升老年代的 4 种情况 |
| 3 | GC 算法 | 三种算法优缺点 + 分别用在哪 |
| 4 | CMS vs G1 | 核心区别表 + 各自适用场景 |
| 5 | Full GC 触发 | 5 种触发条件 + 系统表现 |
| 6 | JIT 分层编译 | C1/C2 区别 + 热点探测机制 |
| 7 | 逃逸分析 | 三种优化(标量替换/锁消除/栈上分配) + "对象一定在堆上吗" |
| 6 | 类加载 | 五阶段 + 双亲委派流程 + 3 种打破场景 |
| 7 | OOM 排查 | HeapDump + MAT 分析链路 |
| 8 | CPU 100% 排查 | 四步法 |
💡 首次全部过一遍 → 第2天只过答不上来的 → 第4天再复习 → 面试前一天最后扫一遍