JVM 面试题笔记

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:

  1. 新对象一般进 Eden;
  2. Eden 满触发 Minor GC ,存活对象年龄 +1,在 S0/S1 之间来回复制;
  3. 年龄达到阈值(默认 15 ,-XX:MaxTenuringThreshold)晋升老年代;
  4. 老年代满触发 Major GC / Full GC(慢,伴随 STW)。

对象相关

对象的创建过程?

  1. 类加载检查:常量池定位类符号引用,未加载则先执行类加载;
  2. 分配内存:指针碰撞(规整堆)/ 空闲列表;TLAB(每个线程在 Eden 私有一小块,避免并发竞争);
  3. 零值初始化:字段赋默认值(0/false/null);
  4. 设置对象头: hashCode、GC 分代年龄、锁标志位、类型指针;
  5. 执行 <init> 构造器(字段显式赋值 + 构造代码块)。

对象在内存中的布局?

  • 对象头 Header:Mark Word(哈希码、GC 年龄、锁标志,64 位机 8 字节)+ 类型指针(压缩后 4 字节)+(数组才有)数组长度;
  • 实例数据:各字段值,相同宽度字段分配在一起;
  • 对齐填充:补齐到 8 字节整数倍。

对象什么时候进入老年代?(高频)

  1. 年龄达到 15(每次 Minor GC 存活 +1);
  2. 大对象直接进老年代 (超大数组,-XX:PretenureSizeThreshold);
  3. 动态年龄判断:Survivor 中同龄对象总大小 > Survivor 一半,该年龄及以上全部晋升;
  4. Minor GC 后 Survivor 放不下的存活对象,直接进老年代(担保机制)。

四种引用的区别?(高频)

引用 回收时机 用途
强引用 只要可达就不回收(普通赋值) 正常业务
软引用 SoftReference 内存不足时才回收 内存敏感缓存
弱引用 WeakReference 下次 GC 必回收(不管内存够不够) ThreadLocalMap 的 key、WeakHashMap
虚引用 PhantomReference 随时可能被回收,仅用于回收通知 堆外内存释放(DirectByteBuffer 的 Cleaner)

什么是逃逸分析?有什么优化?

分析对象的作用域是否会逃出方法/线程。不逃逸时可做:

  1. 栈上分配(标量替换):对象拆成基本类型分配在栈上,方法结束自动回收,减轻 GC;
  2. 锁消除:局部对象的同步锁直接去掉(如方法内 new StringBuffer().append());
  3. 同步省略/标量替换:聚合量还原成标量。

垃圾回收基础

怎么判断对象可以回收?(超高频)

  • 引用计数法 :引用 +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,修正并发期间变动)→ 并发清除。

缺陷:

  1. CPU 敏感:并发阶段抢占用户线程资源;
  2. 浮动垃圾 :并发标记期间产生的新垃圾本轮无法回收,要预留空间 → CMSInitiatingOccupancyFraction 太高会 Concurrent Mode Failure,退化成 Serial Old 全停顿;
  3. 内存碎片:标记-清除不整理 → 大对象分配失败触发 Full GC;
  4. 无法处理浮动垃圾导致的兜底失败。

G1 的原理和特点?(高频)

  1. Region 化堆:把堆切成 2048 个左右等大 Region,每块动态扮演 Eden/Survivor/Old/Humongous(大对象专区);
  2. 可预测停顿 :-XX:MaxGCPauseMillis=200,G1 按各 Region 的回收价值(垃圾比例)排序,优先回收价值高的(Garbage First 名字由来);
  3. 回收流程类似 CMS:初始标记 → 并发标记 → 最终标记 → 筛选回收(STW 复制存活对象到空 Region);
  4. 整体是标记-整理/复制 ,无内存碎片;
  5. 适合大堆(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)机制。


类加载机制

类加载过程五个阶段?(高频)

  1. 加载:读取字节码生成 Class 对象(方法区存类元数据);
  2. 验证:文件格式、元数据、字节码、符号引用合法性;
  3. 准备 :静态变量分配内存并赋零值 (static int a = 1 此时 a = 0;static final 常量直接赋值);
  4. 解析:符号引用 → 直接引用;
  5. 初始化 :执行类构造器 <clinit>()(静态变量赋值 + static 块合并),懒加载,首次主动使用时才触发。

主动引用(触发初始化):new、访问静态变量/方法(非 final 常量)、反射、子类初始化触发父类。被动引用不触发:通过子类引用父类静态变量、数组定义、访问编译期常量。

双亲委派模型?(超高频)

类加载器层级:Bootstrap(核心类库 rt.jar)→ Extension/Platform → Application(classpath)→ 自定义。

加载请求先委派给父加载器,父加载器无法完成(找不到类)才自己加载。

作用:

  1. 安全:防止核心类被篡改(自己写个 java.lang.String 不会被加载);
  2. 避免重复加载:同一个类只会被加载一次(类的唯一性 = 加载器 + 类名)。

怎么破坏双亲委派?为什么破坏?

重写 loadClass()(保留 findClass 语义)不走委派。典型场景:

  1. SPI (JDBC):核心库加载的 DriverManager 需要加载 classpath 的厂商驱动 → 线程上下文类加载器反向委派;
  2. Tomcat:每个 Web 应用独立 WebAppClassLoader,应用间隔离、应用优先于容器(先自己加载);
  3. 热部署/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 怎么排查?

  1. 参数预留现场:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dump/;
  2. MAT / JProfiler 分析 dump:看支配树(Dominator Tree)找最大的对象、GC Roots 引用链定位泄漏源;
  3. 常见原因:集合只加不删(静态 Map 缓存无上限)、连接/流未关闭、ThreadLocal 未 remove、大对象一次性加载(如全表导出)、堆太小。

频繁 Full GC 怎么排查?

  1. jstat -gcutil <pid> 1000 看 GC 频率和各代占用;
  2. 确认触发原因:老年代满?元空间满?(jstat -gcmetacapacity、GC 日志原因字段);
  3. 老年代满:dump 分析大对象/缓存无界;元空间满:动态代理类过多;
  4. 显式 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 日志分析
相关推荐
我的golang之路果然有问题9 个月前
linux 个人笔记导出之网络,防火墙,定时,权限,后台
linux·运维·服务器·网络·笔记·个人笔记
我的golang之路果然有问题9 个月前
mysql 个人笔记导出之-数据库时间戳问题以及增删改查
数据库·笔记·学习·mysql·分享·个人笔记