JVM 面试题笔记
个人复习笔记,覆盖 JVM 内存结构、垃圾回收、类加载、线上问题排查的高频面试题(以 HotSpot / JDK 8+ 为准)。
目录
内存结构
JVM 运行时数据区有哪些?(超高频)
| 区域 | 线程私有? | 存什么 | 异常 |
|---|---|---|---|
| 程序计数器 | ✔ | 当前线程执行的字节码行号,唯一不会 OOM 的区域 | --- |
| 虚拟机栈 | ✔ | 栈帧(局部变量表、操作数栈、动态链接、返回地址) | StackOverflowError、OOM |
| 本地方法栈 | ✔ | native 方法调用 | 同上 |
| 堆 | ✘ 共享 | 对象实例、数组,GC 主战场;分新生代(Eden+2*Survivor)/老年代 | OOM |
| 方法区 | ✘ 共享 | 类信息、常量、静态变量;JDK 8 后由元空间实现(本地内存) | OOM(元空间溢出) |
| 运行时常量池 | 方法区一部分 | 编译期常量 + 运行期常量(String.intern 入池) | OOM |
| 直接内存 | --- | NIO DirectByteBuffer,不受堆大小限制但受物理内存限制 | OOM |
JDK 7 和 8 方法区的变化?
- JDK 7:方法区由永久代实现(堆的一部分),字符串常量池已移到堆;
- JDK 8:废弃永久代,改为元空间 Metaspace ,使用本地内存 (默认无上限,一般设
-XX:MaxMetaspaceSize)。 - 变化原因:永久代大小固定易 OOM、与堆 GC 耦合、HotSpot 与 JRockit 合并后统一。
栈溢出和堆溢出分别什么场景?
- StackOverflowError:递归太深、调用链过长(-Xss 调大栈大小缓解);
- 堆 OOM:对象太多/太大、内存泄漏累积(-Xmx 调大 + 排查泄漏);
- 元空间 OOM:动态生成类过多(CGLIB 代理、Groovy 脚本、大量 JSP)。
堆内新生代和老年代的比例与流转?
默认比例 新生代 : 老年代 = 1 : 2 ,新生代内部 Eden : S0 : S1 = 8 : 1 : 1:
- 新对象一般进 Eden;
- Eden 满触发 Minor GC ,存活对象年龄 +1,在 S0/S1 之间来回复制;
- 年龄达到阈值(默认 15 ,
-XX:MaxTenuringThreshold)晋升老年代; - 老年代满触发 Major GC / Full GC(慢,伴随 STW)。
对象相关
对象的创建过程?
- 类加载检查:常量池定位类符号引用,未加载则先执行类加载;
- 分配内存:指针碰撞(规整堆)/ 空闲列表;TLAB(每个线程在 Eden 私有一小块,避免并发竞争);
- 零值初始化:字段赋默认值(0/false/null);
- 设置对象头: hashCode、GC 分代年龄、锁标志位、类型指针;
- 执行
<init>构造器(字段显式赋值 + 构造代码块)。
对象在内存中的布局?
- 对象头 Header:Mark Word(哈希码、GC 年龄、锁标志,64 位机 8 字节)+ 类型指针(压缩后 4 字节)+(数组才有)数组长度;
- 实例数据:各字段值,相同宽度字段分配在一起;
- 对齐填充:补齐到 8 字节整数倍。
对象什么时候进入老年代?(高频)
- 年龄达到 15(每次 Minor GC 存活 +1);
- 大对象直接进老年代 (超大数组,
-XX:PretenureSizeThreshold); - 动态年龄判断:Survivor 中同龄对象总大小 > Survivor 一半,该年龄及以上全部晋升;
- Minor GC 后 Survivor 放不下的存活对象,直接进老年代(担保机制)。
四种引用的区别?(高频)
| 引用 | 回收时机 | 用途 |
|---|---|---|
| 强引用 | 只要可达就不回收(普通赋值) | 正常业务 |
| 软引用 SoftReference | 内存不足时才回收 | 内存敏感缓存 |
| 弱引用 WeakReference | 下次 GC 必回收(不管内存够不够) | ThreadLocalMap 的 key、WeakHashMap |
| 虚引用 PhantomReference | 随时可能被回收,仅用于回收通知 | 堆外内存释放(DirectByteBuffer 的 Cleaner) |
什么是逃逸分析?有什么优化?
分析对象的作用域是否会逃出方法/线程。不逃逸时可做:
- 栈上分配(标量替换):对象拆成基本类型分配在栈上,方法结束自动回收,减轻 GC;
- 锁消除:局部对象的同步锁直接去掉(如方法内 new StringBuffer().append());
- 同步省略/标量替换:聚合量还原成标量。
垃圾回收基础
怎么判断对象可以回收?(超高频)
- 引用计数法 :引用 +1 失联 -1,为 0 回收。缺陷:循环引用无法回收(JVM 不采用);
- 可达性分析法(JVM 采用) :从 GC Roots 出发搜索,不可达的对象可回收。
GC Roots 包括:虚拟机栈(栈帧局部变量)引用的对象、方法区类静态变量、方法区常量、JNI(本地方法栈)引用的对象、活跃线程等。
面试追问"循环引用为什么不是问题":两个互相引用的对象,从 GC Roots 都不可达,照样回收------可达性分析天然解决循环引用。
三大垃圾回收算法的优缺点?(高频)
| 算法 | 过程 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| 标记-清除 | 标记存活 → 清除垃圾 | 简单 | 内存碎片、效率不稳定 | CMS(老年代) |
| 标记-复制 | 存活对象复制到另一半 → 整块清空 | 无碎片、效率高 | 浪费一半内存 | 新生代(存活少) |
| 标记-整理 | 标记 → 存活对象向一端移动 → 清理边界 | 无碎片、不浪费内存 | 移动对象成本高 | 老年代 |
safe point(安全点)是什么?
GC 需要线程都停在一致状态才能开始标记。线程运行中会在特定位置(方法调用、循环跳转、异常跳转)主动"汇报"自己到达安全点;GC 时等待/中断所有线程到安全点后开始。长期运行的循环(如大 int 循环)可能迟迟到不了安全点 → GC 停顿拉长(JDK 10 引入循环内安全点缓解)。
垃圾收集器
各垃圾收集器一览?(高频)
| 收集器 | 作用区域 | 算法 | 特点 |
|---|---|---|---|
| Serial / Serial Old | 新生代/老年代 | 复制 / 整理 | 单线程 STW,客户端小内存 |
| ParNew | 新生代 | 复制 | Serial 多线程版,配合 CMS |
| Parallel Scavenge / Old | 两代 | 复制 / 整理 | 吞吐量优先(JDK 8 默认) |
| CMS | 老年代 | 标记-清除 | 并发收集、低停顿,JDK 9 废弃 14 移除 |
| G1 | 整堆(Region 化) | 复制+整理 | 可预测停顿,JDK 9+ 默认 |
| ZGC | 整堆 | 着色指针+读屏障 | 停顿 <1ms,TB 级堆,JDK 11+ 实验/15 正式 |
| Shenandoah | 整堆 | 转发指针 | OpenJDK,低停顿 |
CMS 的工作流程与缺陷?(高频)
四阶段:初始标记 (STW,只标记 GC Roots 直接关联)→ 并发标记 (与用户线程并发)→ 重新标记 (STW,修正并发期间变动)→ 并发清除。
缺陷:
- CPU 敏感:并发阶段抢占用户线程资源;
- 浮动垃圾 :并发标记期间产生的新垃圾本轮无法回收,要预留空间 →
CMSInitiatingOccupancyFraction太高会 Concurrent Mode Failure,退化成 Serial Old 全停顿; - 内存碎片:标记-清除不整理 → 大对象分配失败触发 Full GC;
- 无法处理浮动垃圾导致的兜底失败。
G1 的原理和特点?(高频)
- Region 化堆:把堆切成 2048 个左右等大 Region,每块动态扮演 Eden/Survivor/Old/Humongous(大对象专区);
- 可预测停顿 :
-XX:MaxGCPauseMillis=200,G1 按各 Region 的回收价值(垃圾比例)排序,优先回收价值高的(Garbage First 名字由来); - 回收流程类似 CMS:初始标记 → 并发标记 → 最终标记 → 筛选回收(STW 复制存活对象到空 Region);
- 整体是标记-整理/复制 ,无内存碎片;
- 适合大堆(6GB+)、要求可控停顿的服务端应用。
Minor GC、Major GC、Full GC 的区别?(高频)
- Minor GC(Young GC):只回收新生代,频繁、速度快(Eden 满触发);
- Major GC:通常指老年代 GC,说法混乱,很多资料等同于 Full GC;
- Full GC :整堆 + 方法区 回收,最慢。触发:老年代不足、元空间不足、
System.gc()、担保失败、空间分配担保。
对象晋升老年代后 Minor GC 会扫描老年代吗? 不会(否则太慢),靠**卡表(Card Table)**记录老年代指向新生代的引用,Minor GC 只扫描"脏卡"------这就是记忆集(Remembered Set)机制。
类加载机制
类加载过程五个阶段?(高频)
- 加载:读取字节码生成 Class 对象(方法区存类元数据);
- 验证:文件格式、元数据、字节码、符号引用合法性;
- 准备 :静态变量分配内存并赋零值 (
static int a = 1此时 a = 0;static final常量直接赋值); - 解析:符号引用 → 直接引用;
- 初始化 :执行类构造器
<clinit>()(静态变量赋值 + static 块合并),懒加载,首次主动使用时才触发。
主动引用(触发初始化):new、访问静态变量/方法(非 final 常量)、反射、子类初始化触发父类。被动引用不触发:通过子类引用父类静态变量、数组定义、访问编译期常量。
双亲委派模型?(超高频)
类加载器层级:Bootstrap(核心类库 rt.jar)→ Extension/Platform → Application(classpath)→ 自定义。
加载请求先委派给父加载器,父加载器无法完成(找不到类)才自己加载。
作用:
- 安全:防止核心类被篡改(自己写个 java.lang.String 不会被加载);
- 避免重复加载:同一个类只会被加载一次(类的唯一性 = 加载器 + 类名)。
怎么破坏双亲委派?为什么破坏?
重写 loadClass()(保留 findClass 语义)不走委派。典型场景:
- SPI (JDBC):核心库加载的 DriverManager 需要加载 classpath 的厂商驱动 → 线程上下文类加载器反向委派;
- Tomcat:每个 Web 应用独立 WebAppClassLoader,应用间隔离、应用优先于容器(先自己加载);
- 热部署/OSGi:模块化、按需替换类。
线上问题排查
CPU 飙到 100% 怎么排查?(超高频,标准四步)
bash
# 1. 找到占用 CPU 高的 Java 进程
top
# 2. 找到进程内占用 CPU 高的线程(记下 tid)
top -Hp <pid>
# 3. 线程 id 转十六进制
printf '%x\n' <tid>
# 4. 导出线程栈,按十六进制 tid 找到对应的线程在干什么
jstack <pid> | grep -A 20 <nid>
典型结果:死循环、正则回溯、频繁 GC(找到的线程名是 GC 线程,转去查 GC 日志和内存)、序列化热点。
发生 OOM 怎么排查?
- 参数预留现场:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dump/; - MAT / JProfiler 分析 dump:看支配树(Dominator Tree)找最大的对象、GC Roots 引用链定位泄漏源;
- 常见原因:集合只加不删(静态 Map 缓存无上限)、连接/流未关闭、ThreadLocal 未 remove、大对象一次性加载(如全表导出)、堆太小。
频繁 Full GC 怎么排查?
jstat -gcutil <pid> 1000看 GC 频率和各代占用;- 确认触发原因:老年代满?元空间满?(
jstat -gcmetacapacity、GC 日志原因字段); - 老年代满:dump 分析大对象/缓存无界;元空间满:动态代理类过多;
- 显式
System.gc()被调用(RMI 场景)→-XX:+DisableExplicitGC。
内存泄漏和内存溢出的区别?
- 泄漏:不再使用的对象仍被引用,无法回收,慢慢累积(慢性病);
- 溢出 :内存不够分配新对象(可能是泄漏导致,也可能纯粹内存小/突发大对象)。
泄漏积累到一定程度表现为 OOM。
常用参数与工具
常用 JVM 参数?
bash
-Xms4g -Xmx4g # 初始/最大堆(生产上设成一样,避免动态扩容抖动)
-Xmn1g # 新生代大小
-Xss512k # 线程栈大小
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
-XX:SurvivorRatio=8 # Eden:Survivor = 8:1:1
-XX:MaxTenuringThreshold=15
-XX:+UseG1GC -XX:MaxGCPauseMillis=200
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump/
-Xlog:gc*:/data/logs/gc.log # JDK 11+ 统一日志(8 用 -XX:+PrintGCDetails)
常用命令与图形化/在线工具?
| 工具 | 用途 |
|---|---|
jps |
列出 Java 进程 |
jstat -gcutil pid 1000 |
实时观察各代占用与 GC 次数耗时 |
jmap -dump:live,format=b,file=x.hprof pid |
导出堆快照 |
jstack pid |
线程栈(死锁直接显示 Found one Java-level deadlock) |
jinfo |
查看/修改运行时参数 |
| Arthas | 在线诊断神器:dashboard、thread(查 CPU 高线程)、trace(方法耗时链)、watch、heapdump |
| MAT / JProfiler | dump 文件分析 |
| GCViewer / GCEasy | GC 日志分析 |