JVM 五十道核心面试题

以下50道JVM面试题对标阿里P6定级标准,涵盖内存模型、垃圾回收、性能调优、类加载机制、编译优化五大核心模块,按难度梯度编排。


一、JVM内存模型(10题)

Q1:请详细描述JVM的运行时数据区,哪些是线程共享的,哪些是线程私有的?

· 详细答案:
· 线程私有(随线程生灭):①程序计数器(记录执行字节码行号);②虚拟机栈(Java方法执行的栈帧模型,含局部变量表、操作数栈等);③本地方法栈(Native方法服务)。
· 线程共享(随JVM生灭):①堆(存放对象实例和数组,GC主要区域);②方法区(存储类结构、常量池、静态变量、JIT编译代码)。
· 助记口诀:
“两私一计”(线程私有:虚拟机栈、本地方法栈、程序计数器);
“一堆一区”(线程共享:堆、方法区)。
类比:公司里,工位和电脑(私有)是你的,会议室和食堂(共享)是大家的。


Q2:说一下JDK 1.6、1.7、1.8内存区域的变化?

· 详细答案:
· 1.6:永久代(方法区实现)存字符串常量池、静态变量、类信息,有PermSize上限。
· 1.7:将字符串常量池和静态变量从永久代移至堆中。
· 1.8:移除永久代,引入元空间(Metaspace),类信息等移至本地内存(Native Memory)。
· 助记口诀:
“6全包,7搬字,8换本地”
1.6全在永久代;1.7搬走字符串和静态变量;1.8换成元空间(本地内存)。


Q3:为什么要用元空间替代永久代?

· 详细答案:

  1. OOM难控:永久代大小受MaxPermSize硬性限制,项目启动加载大量jar包(如Spring Boot)极易OOM。
  2. 难调优:永久代GC条件苛刻,Full GC才能回收,效率低下。
  3. 本地内存优势:元空间使用本地内存,默认受物理内存上限,且字符串(易OOM元凶)已挪到堆中,永久代失去了必存理由。
    · 助记口诀:
    “永久代太娇气,调大调小都OOM;元空间用物理内存,直接给爷上物理外挂”。

Q4:堆(Heap)和栈(Stack)有什么区别?

· 详细答案:
对比维度 堆(Heap) 栈(Stack)
存储内容 对象实例、数组 局部变量表、操作数栈、方法出口
生命周期 JVM启动到结束 跟随线程/方法调用
内存管理 GC自动回收 自动出栈释放
线程共享 是 否
异常 OutOfMemoryError StackOverflowError(递归过深)/ OOM(动态扩展失败)
· 助记口诀:
“栈存变量堆存物,栈跟线程堆跟VM;栈溢递归堆溢满”。


Q5:对象在堆内存中的布局是怎样的?

· 详细答案(HotSpot):

  1. 对象头(Header):① Mark Word(运行时数据:哈希码、GC分代年龄、锁状态标志,32位占4字节/64位占8字节);② 类型指针(指向方法区类元数据,开启指针压缩时占4字节)。
  2. 实例数据(Instance Data):父类字段 + 子类字段(相同宽度的字段优先分配,如long/double排前面)。
  3. 对齐填充(Padding):保证对象大小是8字节的整数倍(HotSpot内存管理以8字节为单位)。
    · 助记口诀:
    “头(Mark Word)指(类型指针)数(实例数据)填(对齐填充)”。
    联想:快递包裹——面单(头)、地址标签(类型指针)、实物(数据)、泡沫填充(对齐)。

Q6:什么是逃逸分析?有什么作用?

· 详细答案:
· 定义:JIT编译器分析对象的作用域,判断对象是否“逃逸”出方法或线程。
· 作用(三大优化):
①栈上分配:未逃逸对象直接在栈上分配,随方法结束自动销毁,减轻GC压力;
②标量替换:未逃逸对象拆散为基本类型(标量)直接存局部变量,无需创建对象;
③锁消除:若同步锁对象未逃逸,JIT直接去掉锁(如线程安全的StringBuffer)。
· 助记口诀:
“逃逸分析三把斧:栈上分配省GC,标量替换拆对象,锁消除掉白加锁”。


Q7:直接内存(Direct Memory)是什么?

· 详细答案:
· 非JVM规范定义的内存区域,JDK 1.4 NIO引入的堆外内存。
· 通过ByteBuffer.allocateDirect()分配,底层调用malloc。读写时避免中间缓冲区的数据拷贝(零拷贝),适合高频I/O场景(如Netty)。
· 风险:不受-Xmx管控,受物理内存限制,分配回收依赖Cleaner机制,-XX:MaxDirectMemorySize可设上限(默认同-Xmx),未设满时极易忽略的OOM。
· 助记口诀:
“直接内存是外挂,零拷贝快如马;不受堆大小管控,物理内存爆了就抓瞎”。


Q8:方法区、永久代、元空间三者的关系是什么?

· 详细答案:
· 方法区:JVM规范定义的抽象概念(“标准接口”)。
· 永久代 / 元空间:HotSpot对方法区的具体实现(“接口实现类”)。
· 区别:永久代用JVM内存(有上限),元空间用本地内存(无硬上限)。
· 助记口诀:
“接口(方法区)、老实现(永久代)、新实现(元空间)”。
类比:List是接口,ArrayList是老实现(永久代),LinkedList是新实现(元空间)。


Q9:程序计数器为什么是唯一不会OOM的区域?

· 详细答案:
· JVM规范明确规定:程序计数器仅存储当前线程执行的字节码指令地址(或Native方法时的undefined),其内存占用极其固定且极小,没有动态扩展需求,因此JVM规范未定义任何OOM情形(OutOfMemoryError)。
· 助记口诀:
“记个行号不扩展,想溢也溢不出来”。


Q10:32位和64位JVM的最大堆内存分别是多少?

· 详细答案:
· 32位:受操作系统虚拟地址空间限制(Windows约2-3GB,Linux约2-4GB),理论上限约4GB,实际可用通常低于2GB(内核占用部分地址)。
· 64位:理论2^64字节,但受物理内存和GC效率限制(堆过大导致GC停顿飙升),生产环境通常设4-32GB。
· 助记口诀:
“32位撑死4个G,64位理论无限但GC扛不住”。


二、垃圾回收(GC)基础(10题)

Q11:如何判断一个对象可以被回收?

· 详细答案:
· 核心算法:可达性分析(Reachability Analysis)。从一组称为 GC Roots 的根对象出发,向下搜索引用链,不可达的对象即被标记为可回收。
· GC Roots 包括哪些(P6必背清单):
① 虚拟机栈(栈帧中的局部变量表)引用的对象;
② 方法区中静态属性(static)引用的对象;
③ 方法区中常量(final)引用的对象;
④ 本地方法栈中 JNI(Native 方法)引用的对象;
⑤ Java 虚拟机内部的引用(基本数据类型对应的 Class 对象、常驻异常对象等);
⑥ 所有被同步锁(synchronized)持有的对象;
⑦ 反映 Java 虚拟机内部情况的 JMXBean、JVMTI 回调等。
· 注意:即使在可达性分析中不可达,也并非“非死不可”,至少要经历两次标记:若对象未覆盖 finalize() 或已被执行过,则直接回收;否则放入 F-Queue 由低优先级线程执行,若在 finalize() 中自救(重新与 GC Roots 建立关联)则逃过一劫(但官方已废弃此机制,禁止依赖)。
· 助记口诀:
“GC Roots 四类源:栈中变量、静(静态)常量、JNI 引、锁对象和活线程”。
记法:想象 GC Roots 是警察局的警力,从这些据点出发,找不到关联的“黑户”对象统统抓走。


Q12:Java为什么不用引用计数法?

· 详细答案:
· 引用计数法在每个对象头里维护一个计数器,引用+1,引用失效-1,为0即回收。
· 致命缺陷:无法解决循环引用。例如 A 引 B,B 引 A,除此之外再无别人引用它们,计数器永远为1,导致内存泄漏。而可达性分析算法天生能解决循环依赖,因为从 GC Roots 出发根本找不到这个闭环。
· 助记口诀:
“引用计数数不清,循环依赖成死结;可达分析一条路,根上找不到就回收”。


Q13:强引用、软引用、弱引用、虚引用的区别?

· 详细答案:
引用类型 回收时机 典型应用
强引用 永不回收(OOM也不收) new Object()
软引用 内存不足时(OOM前)回收 内存敏感缓存(如图片缓存)
弱引用 下次 YGC 即回收(无论内存是否充足) ThreadLocal 的 Key、WeakHashMap
虚引用 任何时候都可能被回收,无法通过它获取对象实例 跟踪对象回收(如 NIO 直接内存的清理)
· P6 加分点:ThreadLocal 内存泄漏就是因为 Key 是弱引用(GC 时被回收变成 null),但 Value 是强引用导致无法访问。解决方式是每次使用完手动 remove()。
· 助记口诀:
“强死不收,软穷才收,弱见(GC)就收,虚形同虚设只为跟踪”。


Q14:垃圾收集有哪些算法?各自特点是什么?

· 详细答案:
· 标记-清除(Mark-Sweep):先标记垃圾,再清除。缺点:内存碎片严重,大对象无法分配时提前触发 Full GC。
· 标记-复制(Mark-Copy):将内存平分为两块,只使用一块,存活对象复制到另一块后清空原块。缺点:内存利用率只有 50%(HotSpot 优化为 Eden:Survivor=8:1:1,利用率 90%)。优点:无碎片,分配内存只需移动指针(指针碰撞),效率极高。
· 标记-整理(Mark-Compact):标记后让存活对象向一端移动,然后清理边界外内存。优点:无碎片;缺点:移动成本高,STW 时间长。
· 助记口诀:
“清除(Sweep)留碎片,复制(Copy)耗一半,整理(Compact)挪一挪”。


Q15:HotSpot为什么要分为新生代和老年代?

· 详细答案:
· 基于 分代收集理论(弱分代假说 + 强分代假说):
· 弱分代假说:绝大多数对象(约 98%)朝生夕死,放在新生代,用复制算法高频清理,成本极低。
· 强分代假说:熬过多次 GC 的对象(大对象、业务核心对象)难以消亡,放到老年代,用标记-整理(或 CMS 标记-清除)算法降低扫描频率。
· 如果不分代,每次全堆扫描,对象越多 STW 越长。分代是 “分而治之” 的经典思想。
· 助记口诀:
“新生代里淘金子(高频回收),老年代里养大佬(低频扫描)”。


Q16:新生代为什么分为 Eden 和两个 Survivor 区?

· 详细答案:
· 结构:HotSpot 默认 Eden : S0 : S1 = 8 : 1 : 1。
· 为什么是两个 Survivor:为了解决 “碎片化” 和 “内存利用率” 的矛盾。
· 如果只有一个 Survivor,对象从 Eden 存活后复制到 S,下次 GC 时 Eden+S 存活对象无处可去(复制算法要求一块空的保留区)。
· 两个 Survivor(From 和 To)轮流扮演“空盘子”角色:每次 YGC,存活对象复制到 空的 To 区,清空 Eden 和 From,然后 From 和 To 交换身份。保证了内存连续(无碎片)且利用率达 90%。
· 助记口诀:
“伊甸园(Eden)生对象,俩幸存者(S0/S1)倒班扛;你倒过来我清空,复制算法无碎片”。


Q17:对象什么时候会进入老年代?

· 详细答案(P6 必须答全 4 点):

  1. 年龄阈值:每熬过一次 YGC 年龄+1,超过 -XX:MaxTenuringThreshold(默认 15)晋升。
  2. 动态年龄判定(HotSpot 特有):若 Survivor 区中相同年龄的所有对象大小总和 > Survivor 区空间的一半,则年龄 ≥ 该年龄的对象直接晋升(无需熬到 15)。
  3. 大对象直接分配:-XX:PretenureSizeThreshold 参数设置(如 > 3MB),直接进老年代,避免在新生代频繁复制。
  4. Survivor 空间不足:Eden 存活对象 > To 区容量,多余对象直接晋升老年代(分配担保机制)。
    · 助记口诀:
    “熬到十五升,同龄过半升,个头太大升,装不下也升”。

Q18:Minor GC 和 Full GC 有什么区别?

· 详细答案:
| 类型 | 发生区域 | 频率 | STW 时长 | 触发条件 |
|—|—|—|—|
| Minor GC(YGC) | 新生代 | 高(频繁) | 短(几 ms~几十 ms) | Eden 区满 |
| Full GC( FGC) | 整个堆 + 元空间 | 低 | 长(几百 ms~几 s) | 老年代 / 元空间不足等 |
· 注意:Major GC 通常指清理老年代,但很多场景下 Major GC = Full GC,面试时建议明确区分概念。
· 助记口诀:
“新生代满就 YG,全堆满了才 FG;YG 快如闪电,FG 重如泰山”。


Q19:什么情况会触发 Full GC?

· 详细答案(P6 必须列举 5 种以上):
① 老年代空间不足(最典型,如大对象连续分配、晋升失败);
② 元空间(Metaspace)达到阈值(MaxMetaspaceSize 设置过小或加载类过多);
③ System.gc() 显式调用(可通过 -XX:+DisableExplicitGC 屏蔽,但慎用);
④ CMS GC 并发模式失败(Concurrent Mode Failure):CMS 正在并发回收时,老年代又被填满,JVM 自动降级为 Serial Old 进行 Full GC(STW 极长);
⑤ 晋升失败(Promotion Failure):YGC 时 Survivor 放不下,老年代也放不下;
⑥ 统计信息导致的悲观策略:YGC 前估算老年代剩余空间 < 历次晋升平均大小,直接触发 Full GC。
· 助记口诀:
“老元(老年代/元空间)不够,System 显式调,CMS 并发失败,晋升没地儿要”。


Q20:永久代中会发生垃圾回收吗?

· 详细答案:
· 会的。主要回收两部分:废弃常量(如字符串常量池中无引用的字符串)和 无用的类。
· 类的回收条件极其苛刻(必须同时满足):
① 该类的所有实例已被回收(堆中无对象);
② 加载该类的 ClassLoader 已被回收;
③ 该类的 java.lang.Class 对象无任何引用,且无法通过反射访问。
· 场景:OSGi、热部署、大量使用反射生成代理类(如 CGLIB)的应用中,若不回收永久代/元空间,极易 OOM。
· 1.8 后:元空间虽然用本地内存,但依旧会 GC(通过 -XX:+CMSClassUnloadingEnabled 可开启类卸载)。
· 助记口诀:
“永久代里也扫街,常量类都回收;条件苛刻三大条:实例空、加载器亡、Class 对象没人要”。


三、垃圾回收器(10题)

Q21:常见的垃圾回收器有哪些?

· 详细答案(按代际分组):
· 新生代:Serial(单线程,Client模式)、ParNew(多线程,常配CMS)、Parallel Scavenge(吞吐量优先,JDK8默认)。
· 老年代:Serial Old(单线程)、Parallel Old(多线程,吞吐量优先)、CMS(并发低延迟)。
· 整堆(不分代):G1(逻辑分代,物理分区,JDK9+默认)、ZGC(亚毫秒级,JDK11引入)、Shenandoah(JBoss出品)。
· 助记口诀:
“串行并行老中青,CMS低延迟,G1分区管,ZGC无痕流”。
联想:像交通工具——Serial是自行车,Parallel是高铁(吞吐),CMS是地铁(站点多但快),G1是智能交通系统。

Q22:CMS收集器的工作原理和优缺点是什么?

· 详细答案(P6必画流程图):
· 四步走:①初始标记(STW,仅标记GC Roots直接关联对象,极快)→ ②并发标记(用户线程同时跑,从根遍历整个老年代,耗时但不停顿)→ ③重新标记(STW,处理并发期间新产生的引用变化,比初始标记稍长)→ ④并发清除(用户线程同时跑,清除垃圾)。
· 优点:低停顿,追求最短GC停顿时间,适合Web交互应用。
· 缺点(P6必答三点):
①CPU敏感:并发阶段占用CPU资源(默认开启的线程数=(CPU核数+3)/4),可能拖慢业务;
②浮动垃圾:并发清除时业务线程仍在产生垃圾,只能等下次GC,需要预留空间(默认-XX:CMSInitiatingOccupancyFraction=92%);
③内存碎片:采用标记-清除算法,大对象分配失败时退化为Serial Old(STW灾难)。
· 助记口诀:
“初标快,并发扫,重标补漏,并发清;低延是亮点,碎浮(碎片+浮动垃圾)是硬伤”。

Q23:G1收集器的核心设计是什么?

· 详细答案:
· Region化:将堆划分为大小相等的Region(默认2048个,大小1MB~32MB),不再分代连续物理隔离。Eden、Survivor、Old、Humongous(大对象专用Region)都是Region的逻辑角色。
· 可预测停顿模型:维护一个停顿预测模型,根据历史数据估算每个Region的回收收益(垃圾堆积量/回收耗时),维护优先级列表,每次只选收益最高的Region集合(CSet)回收,满足 -XX:MaxGCPauseMillis(默认200ms)。
· 整体标记-整理,局部标记-复制:全局无碎片,YGC采用复制算法。
· 助记口诀:
“大堆划成棋盘格(Region),哪块垃圾多先扫哪(优先列表),停时可控算得准(预测模型),整体整理没碎片”。

Q24:CMS和G1的核心区别是什么?

· 详细答案:
对比维度 CMS G1
回收范围 老年代 全堆(新生代+老年代)
内存布局 连续分代(Eden/S0/S1/Old) 非连续Region(逻辑分代)
算法 标记-清除(有碎片) 标记-整理 + 标记-复制(无碎片)
停顿目标 尽量低(不可控) 可预测(用户指定MaxGCPauseMillis)
Full GC退路 退化为Serial Old(STW极长) 若预测失败,退化为单线程Full GC(但概率低)
· 助记口诀:
“CMS只管老年代,G1全堆一把抓;CMS清出碎玻璃,G1整理亮堂堂;CMS尽量低,G1我说了算”。

Q25:G1如何解决漏标问题?

· 详细答案(P6核心技术点):
· 使用 SATB(Snapshot-At-The-Beginning) 算法。并发标记开始时,对当前存活对象图做逻辑“快照”。
· 写屏障(Write Barrier):在并发标记期间,如果某个对象的引用字段发生变化(如 A.b = null),写屏障会将删除前的旧引用对象记录到SATB队列中(保证快照完整性)。
· 最终标记阶段:处理SATB队列中的引用,确保在初始快照中活着的对象,即使后续引用断了,仍被标记为存活(保守但正确)。
· 对比CMS:CMS用增量更新(Incremental Update)记录新引用,G1用SATB记录旧引用,SATB能更好应对“浮动垃圾”且减少最终标记的STW。
· 助记口诀:
“并发标记拍快照(SATB),引用变了记旧情(旧引用进队列),保守标记不漏网,最终标记扫尾巴”。

Q26:ZGC的特点是什么?

· 详细答案(P6前瞻性知识):
· 核心技术:染色指针(Colored Pointers) + 读屏障(Load Barrier)。将对象引用(64位指针)的高4位作为状态标记位(Marked0/Marked1/Remapped),GC并发时无需修改对象头,通过指针染色即可判断对象状态。
· 极致低延迟:停顿时间 < 1ms(JDK 17平均0.5ms),且不随堆大小增加(10GB和1TB停顿几乎一致)。
· 支持:JDK 11实验,JDK 15正式,JDK 21引入分代ZGC,进一步降低卡顿20倍,提升吞吐量。
· 缺点:牺牲部分吞吐量(约5%~15%),适合大堆内存(>16GB)且对延迟极敏感的场景(如金融高频、AI推理)。
· 助记口诀:
“指针染颜色(染色指针),读屏(读屏障)挡病毒;十G和TB一个样,亚毫秒级真大哥”。

Q27:什么场景下适合用G1?

· 详细答案(选型实战):
① 堆内存较大(> 4GB ~ 6GB),CMS在4GB以上碎片问题恶化;
② 对响应时间有明确SLA要求(如接口RT < 200ms),而非极端吞吐量;
③ 应用在JDK 9+(G1已成默认);
④ 业务流量不均匀,但不想像CMS那样精细调优(G1自适应参数能力强)。
· 助记口诀:
“大堆(>4G)要快(RT敏感)选G1,JDK高版本不用想”。

Q28:什么是STW(Stop The World)?

· 详细答案:
· 垃圾回收时,JVM暂停所有应用线程(Java代码和Native代码除外),进入安全点(Safepoint)后执行GC操作。
· 为什么必须有STW:枚举GC Roots时,需要“冻结”对象引用关系快照,否则一边标记对象一边改引用,永远标记不完。
· 优化:减少STW时长是所有GC演进的核心方向(G1/ZGC通过并发标记、预读取、并行处理不断缩短STW)。
· 助记口诀:
“GC一响,全员冻僵;不冻僵,数不清,老祖宗定的规矩”。

Q29:CMS的增量更新和G1的SATB有什么区别?

· 详细答案(P6高难度区分,必背):
· CMS-增量更新(Incremental Update):在并发标记中,写屏障记录新增的引用关系(如 A.c = C_new)。最终标记时需要重新扫描这些新增引用的对象,追踪新增路径。
· G1-SATB(Snapshot At The Beginning):写屏障记录删除的引用关系(如 A.b = null 中的 A.b 原对象)。最终标记时扫描所有SATB队列中的旧引用,确保快照时刻存活对象全部保留。
· 本质差异:增量更新是“抓新增”防漏,SATB是“抓删除”防漏。SATB对“浮动垃圾”更宽容(多留一些垃圾),但减少了最终标记时重新扫描的开销,所以G1的最终标记STW通常小于CMS的重新标记。
· 助记口诀:
“CMS管新增(新箭头),G1管旧情(断箭头);新增改得快,旧情更周全”。

Q30:ParNew和Parallel Scavenge的区别?

· 详细答案:
· ParNew:多线程并行复制算法的新生代收集器,核心是配合CMS工作(JDK强制绑定,CMS只能用ParNew或Serial)。
· Parallel Scavenge:多线程并行复制算法的新生代收集器,核心是吞吐量优先(-XX:GCTimeRatio可设GC占总时间比),且支持自适应调节(-XX:+UseAdaptiveSizePolicy),JVM自动调优新生代大小/Survivor比例。
· 一句话总结:ParNew为低延迟(配CMS)而生,Parallel Scavenge为算得完(批处理)而生。
· 助记口诀:
“ParNew是CMS的黄金搭档,Parallel Scavenge是吞吐量的自走棋(自动调参)”。

四、性能调优与问题排查(10题)


Q31:线上系统GC频繁,如何系统性排查和优化?

· 详细答案(P6标准SOP,按顺序执行):
① 监控告警确认:通过Grafana/Prometheus确认GC频率和耗时基线,明确是YGC频繁还是FGC频繁。
② 看GC日志:开启-Xlog:gc*(或-XX:+PrintGCDetails),统计GC间隔、吞吐量、各代空间变化。
③ 看内存使用:用jstat -gc 1s 实时观察Eden/S0/S1/Old/Metaspace的使用率和晋升速率。
④ Dump堆内存:jmap -dump:live,format=b,file=heap.hprof (注意live会触发FGC,谨慎操作)。
⑤ 离线分析:用MAT(Memory Analyzer)打开Dump,看Leak Suspects和Dominator Tree,定位大对象或泄漏源头。
⑥ 调整参数/代码:若大对象过多,调大-XX:PretenureSizeThreshold;若Survivor过小,调整-XX:SurvivorRatio;若内存泄漏,修复代码后上线。
· 助记口诀:
“一看监控二看日志,三用jstat四dump,MAT分析找元凶,调参改码终得解”。
联想:医生看病——验血(日志)、CT(Dump)、病理分析(MAT)、开药(调参)。

Q32:常用的JVM调优参数有哪些?

· 详细答案(按类别记忆,P6必须脱口而出):
· 堆内存:-Xms(初始堆)、-Xmx(最大堆,建议两者相等防扩容抖动)、-Xmn(新生代大小,老年代自动=-Xmx–Xmn)。
· 元空间:-XX:MetaspaceSize(触发FGC阈值)、-XX:MaxMetaspaceSize(上限)。
· GC选型:-XX:+UseG1GC、-XX:+UseConcMarkSweepGC(JDK9后已废弃)、-XX:MaxGCPauseMillis=200(G1停顿目标)。
· 线程:-Xss(线程栈大小,默认1M,若线程数超多可减至256k)。
· OOM兜底:-XX:+HeapDumpOnOutOfMemoryError + -XX:HeapDumpPath=/path(保命必备)。
· GC日志:JDK9+用-Xlog:gc*:file=/path/gc.log:time,uptime,level,tags;JDK8用-XX:+PrintGCDetails -Xloggc:/path/gc.log。
· 助记口诀:
“堆(Xms/Xmx)元(Metaspace)栈(Xss)GC(UseG1),日志(Xlog)Dump(HeapDump)保太平”。

Q33:如何排查CPU飙升问题?

· 详细答案(阿里高频实操题):
① top 找到CPU最高的Java进程PID。
② top -Hp 找到该进程内CPU占用最高的线程ID(十进制)。
③ 将十进制线程ID转为十六进制(printf “%x\n” )。
④ jstack | grep -A 20 <十六进制TID> 导出线程栈,定位到具体代码行(死循环、频繁GC的GCTaskThread、正则回溯等)。
⑤ 补充排查:若jstack发现全是VM Thread或GC task thread,说明是GC频繁导致CPU飙升,需回到Q31的GC排查流程。
· 助记口诀:
“Top查进程,Top -Hp查线程,printf转十六进,jstack一把梭”。

Q34:OOM有哪些类型?分别如何排查?

· 详细答案(P6必须分门别类):
① 堆OOM(java.lang.OutOfMemoryError: Java heap space):最常见。jmap dump后用MAT看Histogram,关注byte[]、char[]和自定义大对象,排查缓存是否无限膨胀、大List/Map未释放。
② 元空间OOM(Metaspace):jstat -gc观察MC(当前Metaspace)持续上涨。常见于热部署(ClassLoader未卸载)、CGLIB动态代理大量生成类(如Spring AOP)、JSP编译。通过-XX:MaxMetaspaceSize设上限并分析类加载情况。
③ 栈OOM(StackOverflowError / unable to create new native thread):前者是递归无出口,检查递归代码;后者是线程数超系统限制(ulimit -u),或-Xss过大导致内存不足以创建新线程。
④ 直接内存OOM(Direct buffer memory):NIO/Netty使用ByteBuffer.allocateDirect()未释放。排查-XX:MaxDirectMemorySize是否设得过小,用jcmd VM.native_memory查看本地内存分配。
· 助记口诀:
“堆溢查缓存,元溢看类载,栈溢查递归,直溢追NIO”。

Q35:如何通过GC日志分析问题?

· 详细答案(P6必须看懂日志里面的隐含指标):
· 关注四个核心指标:
① GC频率:每分钟YGC次数(正常几十次/分钟,若>100次且持续,说明新生代太小或分配速率过快)。
② 晋升速率(Promotion Rate):每次YGC后Old区增长的速率。若Old区每小时增长>20%,说明大量对象生命周期刚好跨过YGC,需调大新生代。
③ 停顿时间(Pause Time):User(用户态CPU)、Sys(内核态)、Real(实际停顿)。若Real远大于User+Sys,说明IO负载高或Swap导致。
④ GC前后空间变化:YGC后Eden接近0,Survivor占用率 > 90%,说明对象存活率高,需调大Survivor或直接晋升阈值。
· 工具辅助:将日志上传至 GCeasy 或 GCEasy(在线可视化)可秒级出报表,面试时提这个工具是加分项。
· 助记口诀:
“频次看压力,晋升看阈值,停顿看Swap,空间看存活”。

Q36:常用的JVM监控和调优工具有哪些?

· 详细答案(按场景分类):
· 命令行四兄弟:jps(查进程)、jstat(看GC/内存实时)、jmap(dump堆/查看堆详情)、jstack(看线程栈)。jinfo(动态修改部分参数)是隐藏的第五个。
· 图形界面:VisualVM(自带,插件丰富)、JConsole(轻量级)。
· 离线分析(大杀器):Eclipse MAT(分析内存泄漏最强)、JProfiler(商业,功能全面)。
· 线上无侵入(阿里最爱):Arthas(阿尔萨斯)——阿里开源,不用重启即可在线watch方法入参出参、trace调用链路、dashboard看实时面板、jad反编译线上代码,P6必备利器!
· 助记口诀:
“命令四兄弟(stat/map/stack/jps),可视化看Visual,大Dump找MAT,线上救火Arthas”。

Q37:内存泄漏如何定位?

· 详细答案(P6核心排查能力):
· 现象特征:老年代(Old Gen)使用量持续缓慢上升,即使FGC后也回不到初始水位(正常情况FGC后应大幅下降)。
· 定位步骤(MAT三板斧):
① 用jmap连续dump两次(间隔1小时),或用jstat确认泄漏趋势。
② 打开MAT,使用 Leak Suspects Report 直接看嫌疑报告。
③ 使用 Dominator Tree(支配树)看谁占着最大的内存不释放。重点关注GC Roots Path(到GC Roots的最短路径),看谁在引用它。
· 常见泄漏源头(五大致命伤):
① 全局静态容器(static List、static Map)无限add,忘了remove。
② 缓存框架(Guava Cache/Caffeine)未设置过期时间或最大容量。
③ ThreadLocal 未 remove()(Key是弱引用会被回收,但Value是强引用卡在Thread的Entry里,线程池复用线程时尤其致命)。
④ 各类连接(数据库Connection、IO流)未在finally中关闭。
⑤ 监听器/回调注册后未注销。
· 助记口诀:
“老年代只涨不跌,MAT查根路径;泄漏源头五大坑:静态容器、缓存、ThreadLocal、连接、监听器”。

Q38:8G内存的服务器,如何设置JVM参数?

· 详细答案(经典场景题,必须结合实际):
· 堆内存:-Xms4g -Xmx4g(留一半给OS和堆外,防止OOM Killer)。若容器化(K8s),需加上-XX:+UseContainerSupport(JDK8u191+默认开启,保证感知容器内存限制)。
· 元空间:-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m(避免频繁扩容触发FGC)。
· GC选型(二选一):若RT敏感且堆>4G,选G1:-XX:+UseG1GC -XX:MaxGCPauseMillis=200;若为批处理/吞吐量优先,选Parallel:-XX:+UseParallelGC -XX:+UseParallelOldGC。
· 保命参数:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump/ -Xlog:gc*:file=/data/logs/gc.log:time,uptime:filecount=10,filesize=50M。
· 踩坑提醒:总内存 - Xmx 至少留 1.5G~2G,否则元空间、堆外内存、线程栈一抢,直接触发系统OOM Killer杀进程!
· 助记口诀:
“堆设4G留一半,元空间设256,G1保RT日志全,Dump保命防崩盘”。

Q39:Full GC频繁但堆内存使用率仅60%,可能是什么原因?

· 详细答案(P6高阶陷阱题,区分P6和P7的关键):
这题不能只会答“老年代满了”!必须分点说出隐蔽原因:
① 元空间(Metaspace)扩容触发:-XX:MetaspaceSize(初始阈值)设置过小,类加载达到阈值触发FGC扩容。解决方法:将MetaspaceSize设大(如256m)。
② 显式调用 System.gc():代码中或三方库(如RMI/Distributed GC)调用了System.gc()。检查方式:加-XX:+DisableExplicitGC屏蔽,看FGC是否消失。
③ CMS并发模式失败(Concurrent Mode Failure):虽然Old占60%,但碎片严重,无法容纳一个大对象(如超大数组),导致分配失败,JVM降级为Serial Old做FGC。看GC日志关键字“Concurrent Mode Failure”。
④ 大对象直接进老年代:-XX:PretenureSizeThreshold设得过小,大量大对象直扑Old,但Old虽有空间却因不连续无法分配。
⑤ G1的Humongous分配:G1中大对象(超过Region 50%)直接分配在Humongous Region,若连续Humongous区不够,触发FGC(即使总体内存60%)。
· 助记口诀:
“堆没满却FGC,元空间扩容惹的祸(Metaspace),显式调用看Disable,CMS碎片塞不进(Concurrent Failure),大对象直扑老年代”。

Q40:如何主动通知JVM进行垃圾回收?

· 详细答案(考察底层认知):
· 调用 System.gc() 或 Runtime.getRuntime().gc()。JVM规范明确:这只是“建议(Suggestion)”,JVM不保证立即执行(由-XX:+DisableExplicitGC控制是否忽略)。
· 生产环境禁忌与建议:原则上禁止显式调用,因为会触发Full GC搞垮性能。
· 特殊情况(P6必知):NIO的DirectByteBuffer分配堆外内存时,依赖System.gc()触发Cleaner线程回收。如果生产禁用了System.gc()(-XX:+DisableExplicitGC),可能导致直接内存OOM。最佳实践:用 -XX:+ExplicitGCInvokesConcurrent 替代完全禁用,让System.gc()触发的FGC转为并发GC(CMS/G1),既回收了直接内存,又不至于STW卡死。
· 助记口诀:
“System.gc是建议,JVM大爷不一定听;生产禁用是常态,堆外内存例外得慎行(配ExplicitGCInvokesConcurrent)”。

五、类加载机制与字节码(5题)


Q41:Java类加载的全过程是什么?

· 详细答案(P6必须按生命周期顺序,一字不差):
① 加载(Loading):通过类的全限定名获取二进制字节流(可来自jar、网络、动态生成),将静态存储结构转为方法区的运行时数据结构,在堆中生成java.lang.Class对象作为访问入口。
② 验证(Verification):确保字节码符合JVM规范,不会危害JVM安全(文件格式验证、元数据验证、字节码验证、符号引用验证)。可跳过:-Xverify:none可关闭以提升启动速度(但生产慎用)。
③ 准备(Preparation):为静态变量(static)分配内存并设置默认零值(如int=0, reference=null)。注意:static final常量在编译期就确定值,这里直接赋真实值(不是零值)。
④ 解析(Resolution):将常量池中的符号引用(如#1 = Methodref)替换为直接引用(内存中的实际地址)。可能触发其他类的加载。
⑤ 初始化(Initialization):执行类构造器(),按代码顺序执行静态变量赋值和静态代码块。这是真正执行Java代码的阶段。如果父类未初始化,先初始化父类。
⑥ 使用(Using):正常创建对象、调用方法。
⑦ 卸载(Unloading):该类的所有实例被回收、ClassLoader被回收、Class对象无引用时,方法区中的类数据可被卸载(条件苛刻)。
· 助记口诀(一句话串起来):
“加(加载)验(验证)准(准备)解(解析)初(初始化)使(使用)卸(卸载)”。
类比:快递收货(加载)→ 验货(验证)→ 称重贴单(准备)→ 地址翻译(解析)→ 上架开卖(初始化)→ 正常售卖(使用)→ 下架销毁(卸载)。
· P6 核心追问陷阱:准备阶段和初始化阶段的区别是什么?
准备阶段只给static变量赋零值,初始化阶段才赋用户定义值。
例:static int a = 10; → 准备阶段a=0,初始化阶段a=10。
但如果static final int a = 10; → 编译时已确定,准备阶段直接a=10(常量的特殊性)。

Q42:双亲委派模型的工作流程是什么?

· 详细答案(画图记忆法):
· 三层结构:启动类加载器(Bootstrap ClassLoader) ← 父 ← 扩展类加载器(Extension ClassLoader) ← 父 ← 应用程序类加载器(Application ClassLoader)。
· Bootstrap:C++实现,加载<JAVA_HOME>/lib核心类库(如rt.jar)。
· Extension:Java实现,加载<JAVA_HOME>/lib/ext下的类。
· Application:Java实现,加载classpath下的应用类。
· 工作流程(核心规则):
① 一个类加载器收到类加载请求,先不自己加载,把请求委派给父加载器。
② 父加载器又委派给它的父加载器……直到顶级Bootstrap。
③ 如果父加载器找不到(抛ClassNotFoundException),子加载器才尝试自己加载。
· 好处:保证核心API(如java.lang.Object)在所有JVM中唯一且安全——你写一个java.lang.Object根本不会被加载,因为Bootstrap已经加载了。
· 助记口诀:
“孩子遇到事,先找爹,爹再找爷爷;爷爷找不到,才让孩子自己上”。
联想:家里装修(找包工头)——先问爷爷辈的老工匠会不会,不会才问爸爸,爸不会才轮到自己干。

Q43:什么情况下需要破坏双亲委派模型?

· 详细答案(P6高频追问,必须答出3个典型场景):
① SPI(Service Provider Interface)机制(如JDBC、JNDI):
· 问题:JDBC的DriverManager在rt.jar中由Bootstrap加载,但具体数据库驱动(如com.mysql.jdbc.Driver)在应用classpath中,Bootstrap压根找不到。
· 解法:引入线程上下文类加载器(Thread Context ClassLoader)——DriverManager调用Thread.currentThread().getContextClassLoader(),用Application ClassLoader去加载具体驱动。倒过来让父加载器调用了子加载器,直接破坏了双亲委派。
② Web容器(如Tomcat):同一Tomcat下部署多个Web应用,不同应用可能依赖不同版本的Spring(如Spring 4 vs Spring 5)。Tomcat自定义类加载器(WebAppClassLoader)优先自己加载,打破双亲委派实现应用隔离。
③ 热部署(HotSwap)/ OSGi模块化:需要动态替换类实现,自定义类加载器每次都重新加载新版class,旧版卸载,必须打破缓存机制。
· 助记口诀:
“SPI用线程加载器,Tomcat多版本搞隔离,热部署必须自己上”。

Q44:什么是字节码?采用字节码的好处是什么?

· 详细答案:
· 定义:字节码(Bytecode)是JVM的指令集,由操作码(Opcode,如0xB2对应getstatic) + 操作数组成。Java源码经过javac编译生成的.class文件,本质就是一串字节码指令流。
· 两大核心好处:
① 平台无关性(Write Once, Run Anywhere):源码编译一次成字节码,任何平台(Windows/Linux/macOS)只要装了JVM,就能执行相同字节码。
② 语言无关性(Language Interoperability):JVM不看源码,只看字节码。Kotlin、Scala、Groovy、JRuby等只要编译成.class文件,就能在JVM上跑。JVM成了多语言运行时平台。
· P6加分点:字节码还附带安全性检查(类加载时的字节码验证器),比直接编译为机器码的语言(C/C++)更安全。
· 助记口诀:
“字节码是中间件,跨平台跨语言全靠它;javac生产它,JVM消费它”。

Q45:什么是字节码增强技术?

· 详细答案(P6必须讲出技术和应用场景):
· 定义:在运行时或加载时,通过修改或生成新的字节码,改变类的行为或增加功能,不修改原始源码。
· 主流实现技术(按层级从低到高):
① ASM:直接操作字节码指令(类似写汇编),性能最高,但开发难度极大。
② Javassist:通过字符串拼接Java源码的方式生成字节码(类似写C),上手简单,但性能略低。
③ CGLIB:基于ASM封装,Spring AOP中常用(动态代理),通过继承目标类生成子类。
④ ByteBuddy:现代推荐,API友好,比ASM简单且性能接近,被Hibernate、Mockito等广泛使用。
· 典型应用场景(必备案例):
① Spring AOP:运行时动态生成代理类,织入切面逻辑(事务、日志)。
② APM(应用性能监控):SkyWalking、Pinpoint通过java.lang.instrument + 字节码增强,在不改代码的情况下注入监控埋点(方法耗时、调用链)。
③ 热部署工具(JRebel、Arthas的watch/trace命令):动态修改线上方法行为,无需重启。
④ Lombok:在编译期通过注解处理器修改AST(抽象语法树),生成getter/setter/构造器等。
· 助记口诀(按工具定位):
“ASM写汇编,Javassist写C,CGLIB是Spring御用,ByteBuddy新时代一哥;场景AOP、APM、热部署、Lombok四大神器”。

六、编译优化与进阶(5题)
Q46:JVM中new对象时,堆会发生抢占吗?怎么解决?

· 详细答案(P6必知的高并发内存分配细节):
· 会抢占。堆是所有线程共享的,多个线程同时new对象时,都要在Eden区申请内存。如果直接操作堆指针(bump-the-pointer),必须加锁,高并发下会成为性能瓶颈。
· 解决方案:TLAB(Thread Local Allocation Buffer,线程本地分配缓冲区)。
· 每个线程在Eden区预先分配一块私有的连续空间(TLAB,默认约占Eden的1%)。
· 线程new对象时,优先在自己的TLAB中分配,无需加锁(仅需移动指针),效率极高。
· 只有当TLAB空间不足时,才需要同步加锁(CAS重试) 在Eden区重新申请新的TLAB。
· 调优参数:-XX:+UseTLAB(默认开启)、-XX:TLABSize(手动设置大小)、-XX:+PrintTLAB(打印TLAB使用情况)。
· 助记口诀:
“并发New对象,堆上抢指针;TLAB私有化,锁都省了;不够再CAS,性能顶呱呱”。
类比:公司公共笔筒(堆)大家抢,给每人发一支笔(TLAB),笔没墨了再去公共区换。

Q47:什么是JIT编译?和解释执行有什么区别?

· 详细答案(执行引擎的核心二象性):
· 解释执行(Interpreter):JVM启动时,逐条读取字节码指令并翻译成机器码执行。优点:启动快,内存占用小;缺点:执行效率低,重复代码每次都要翻译。
· JIT编译(Just-In-Time Compilation):运行时将热点代码(Hot Spot Code)(如频繁调用的方法、循环体)直接编译成本地机器码并缓存(CodeCache),下次直接执行机器码。优点:执行效率极高(可达解释执行的10~100倍);缺点:启动变慢(需要编译时间),占用CodeCache内存。
· 二者协作(分层编译,JDK7+默认):先解释执行快速启动 → JVM通过计数器(方法调用计数器+回边计数器)识别热点 → 触发JIT编译(C1层做快速优化,C2层做激进优化)。最终热点方法替换为编译后的机器码。
· C1 vs C2:C1(Client Compiler)优化轻量,编译快;C2(Server Compiler)优化激进(如激进内联、逃逸分析),编译慢但性能更高。JDK8默认分层编译(Tiered Compilation),先C1后C2。
· 助记口诀:
“解释执行启动快,JIT编译跑得快;热点计数来触发,C1轻量C2狠”。

Q48:什么是方法内联?

· 详细答案(JIT最重要的优化手段,没有之一):
· 定义:在编译时,将被调用方法的方法体直接复制到调用方方法体中,消除方法调用的开销(压栈、出栈、参数传递)。这是JIT默认进行的优化。
· 为什么重要(蝴蝶效应):内联不仅是省去调用开销,更关键的是为后续更激进的优化创造条件。如果方法没内联,编译器只能“黑盒”看待它;内联后,编译器可以基于上下文做逃逸分析、冗余消除、死码剔除等全局优化。
· 触发条件(阈值):热方法且方法体体积较小(默认-XX:MaxInlineSize=35字节,-XX:FreqInlineSize=325字节)。过大则不会内联(防止代码膨胀)。
· P6实战案例:getter/setter这种小方法几乎100%内联;递归方法、大循环体通常不内联。若发现性能瓶颈在频繁调用小方法上,可手动调大MaxInlineSize。
· 助记口诀:
“调用太费劲,复制方法体;一联解千愁,后续优化全靠它”。
联想:你不会每次都打电话问同事一个公式,而是把公式写在手边(内联),抄一遍直接算更快。

Q49:String a = “123”,String b = new String(“456”),String c = a + b,对JVM来说发生了什么?

· 详细答案(P6必须画出内存图):
· String a = “123”:在字符串常量池(JDK1.7后在堆中)中查找字面量"123"。若不存在,在常量池中创建并让a指向它。a指向堆中常量池的对象。
· String b = new String(“456”):
① 先在字符串常量池中创建或复用字面量"456";
② 在堆中显式创建一个新的String对象,该对象的char[]值拷贝自常量池中的"456";
③ b指向堆中新创建的对象(注意:和常量池的"456"不是同一个对象)。
· String c = a + b:
① javac编译时不会直接用String连接,而是优化为 new StringBuilder() 或 StringBuffer(线程安全场景,但通常编译器用StringBuilder);
② 调用.append(“123”).append(“456”);
③ 最终.toString()生成一个新的堆对象,内容为"123456",c指向这个堆对象。
· 注意:如果是在循环中拼接字符串(如for里str += i),每次循环都会新建一个StringBuilder,性能极差,必须手动用StringBuilder。
· 终极追问:c.intern() 会怎样?→ 将"123456"存入常量池(若不存在),并返回常量池引用。
· 助记口诀:
“双引号进常量池,new String多一份拷贝;加号拼接变Builder,循环千万别这么搞”。

Q50:JDK 8和JDK 17在JVM层面有哪些重要变化?

· 详细答案(展示技术视野,P6-P7过渡题):
· JDK 8 → JDK 9/10/11(过渡期):
· 永久代→元空间(JDK8):已提,核心变化。
· 模块化(JDK 9):引入module-info.java,JVM内部类加载机制对模块路径(ModulePath)和类路径(ClassPath)做了区分,强化了封装性,某些rt.jar中的内部API(如sun.misc.Unsafe部分方法)被限制访问。
· G1成为默认GC(JDK 9):取代Parallel Scavenge + Parallel Old。
· ZGC实验性引入(JDK 11):亚毫秒级低延迟,但彼时还不够成熟。
· JDK 17(LTS,当前P6生产标配)关键JVM演进:
· 分代ZGC正式发布(JEP 439):ZGC不再是不分代的“猛兽”,引入年轻代和老年代,吞吐量提升约20% ~ 30%,卡顿降低20倍,彻底具备线上大规模落地能力。
· 增强G1的并发性:G1的标记-整理进一步并行化,MaxGCPauseMillis预测模型更精准,Full GC发生概率大幅降低。
· CDS(Class Data Sharing)归档增强:支持JDK自身核心类和应用类归档(AppCDS),应用启动速度最多提升30%(尤其适合Serverless/FaaS冷启动场景)。
· 废弃/移除部分垃圾回收器:CMS在JDK14被移除(顺带提一句,展示追踪能力)。
· P6视野建议:如果公司还在JDK8,你说“我们正在调研升级到JDK17,原因是分代ZGC能解决我们大堆内存下的RT毛刺问题,且AppCDS能提升容器冷启动速度”——这句话能让面试官觉得你对技术演进有业务嗅觉。
· 助记口诀(按LTS版本号记):
“8换元空间,9搞模块化,11出ZGC,17的分代ZGC才封神;G1一路在增强,CMS最终归尘土”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值