☕ JVM 专项突击:从内存模型到线上救火(支付中台实战篇)
1. 对象内存流转:从“出生”到“飞升”
面试官:一个
PayOrder对象在 JVM 中经历了什么?
- 分配阶段:对象优先在 Eden 区 分配。栈中存放引用(指针),堆中存放实例。
- 晋升阶段(15岁原则):经历一次 Minor GC 存活的对象移动到 Survivor 区 (S0/S1),年龄 +1,达到
MaxTenuringThreshold(默认 15)后进入 老年代。 - 三条“直达老年代”捷径(必考点):
- 大对象直接入:超过
-XX:PretenureSizeThreshold的对象(如超大对账单 Map)直接进老年代,避免在 S 区来回复制。 - 空间分配担保:Minor GC 时 Survivor 区放不下,直接通过担保机制进入老年代。
- 动态年龄判定:Survivor 区中从小到大累加对象大小,若达到某一年龄 $n$ 时总量超过 S 区 50%,则年龄 $\ge n$ 的对象全部晋升。
- 大对象直接入:超过
2. 垃圾回收器:G1 的“化整为零”与“按时交工”
面试官:为什么支付系统首选 G1 垃圾回收器?
- Region 划分:G1 将堆内存拆分为约 2048 个大小相等的 Region,角色动态变换。
- 核心优势:
- Garbage First:优先回收垃圾最多、回收价值最高的 Region。
- 可预测停顿:通过
-XX:MaxGCPauseMillis=200设置预期停顿目标,G1 会自动调整回收范围,保证支付接口的 低延迟。
- 致命点:若频繁出现 Humongous Region(巨型对象),会导致 G1 回收效率骤降甚至触发 Full GC。
3. 类加载机制:双亲委派与“破局者”
面试官:如何加载两个同名的
PayService.class?
- 双亲委派:向上委托父类加载器处理,确保核心 API(如
String)的安全与唯一。 - 破坏手段:继承
ClassLoader并 重写loadClass()方法(绕过向上委托逻辑)。 - 实战场景:
- Tomcat 隔离:不同 Web 应用依赖不同版本的第三方库。
- 热部署:不重启应用,丢弃旧加载器,创建新加载器重新加载代码。
- SPI 机制:如 JDBC 驱动加载,利用线程上下文类加载器实现逆向调用。
4. 线上救火:OOM 与 Heap Dump 排查
面试官:线上支付接口变慢,Full GC 频繁,你怎么查?
- 第一步:监控确认:使用
jstat -gcutil [pid] 1000观察各区占用、GC 频率及耗时。 - 第二步:导出快照:
- 自动触发:配置
-XX:+HeapDumpOnOutOfMemoryError。 - 手动抓取:使用
jmap -dump:format=b,file=heap.hprof [pid]。
- 自动触发:配置
- 第三步:MAT 分析:
- Dominator Tree:找占用内存最大的对象。
- Path to GC Roots:看谁在引用这个垃圾(解决内存泄漏)。
- 实战案例:“曾排查过因一次性 SELECT 全量对账数据导致 Survivor 区溢出,对象直晋老年代引发频繁 Full GC 的问题,通过优化为分片(Batch)处理后解决。”
5. 栈溢出:StackOverflowError 的“深度”陷阱
面试官:递归写错报 StackOverflowError 怎么办?
- 根因:线程私有的 虚拟机栈 空间固定(默认 1MB),方法调用过深(死循环递归)导致栈帧堆叠撑爆栈空间。
- 排查:直接使用
jstack打印线程堆栈,定位不断重复出现的代码行号。 - 调优思路:
- 优化递归逻辑(改为循环)。
- 若业务确实需要深层调用,调大
-Xss参数。 - 若因“线程开得太多”报 OOM,应 减小
-Xss以容纳更多线程。
💡 李工总结口诀:
“堆溢出看引用(MAT),栈溢出看行数(jstack),高频率看 GC(jstat)。”