JVM Relearn 项目实战

☕ JVM 专项突击:从内存模型到线上救火(支付中台实战篇)

1. 对象内存流转:从“出生”到“飞升”

面试官:一个 PayOrder 对象在 JVM 中经历了什么?

  • 分配阶段:对象优先在 Eden 区 分配。栈中存放引用(指针),堆中存放实例。
  • 晋升阶段(15岁原则):经历一次 Minor GC 存活的对象移动到 Survivor 区 (S0/S1),年龄 +1,达到 MaxTenuringThreshold(默认 15)后进入 老年代
  • 三条“直达老年代”捷径(必考点)
    1. 大对象直接入:超过 -XX:PretenureSizeThreshold 的对象(如超大对账单 Map)直接进老年代,避免在 S 区来回复制。
    2. 空间分配担保:Minor GC 时 Survivor 区放不下,直接通过担保机制进入老年代。
    3. 动态年龄判定:Survivor 区中从小到大累加对象大小,若达到某一年龄 $n$ 时总量超过 S 区 50%,则年龄 $\ge n$ 的对象全部晋升。

2. 垃圾回收器:G1 的“化整为零”与“按时交工”

面试官:为什么支付系统首选 G1 垃圾回收器?

  • Region 划分:G1 将堆内存拆分为约 2048 个大小相等的 Region,角色动态变换。
  • 核心优势
    1. Garbage First:优先回收垃圾最多、回收价值最高的 Region。
    2. 可预测停顿:通过 -XX:MaxGCPauseMillis=200 设置预期停顿目标,G1 会自动调整回收范围,保证支付接口的 低延迟
  • 致命点:若频繁出现 Humongous Region(巨型对象),会导致 G1 回收效率骤降甚至触发 Full GC。

3. 类加载机制:双亲委派与“破局者”

面试官:如何加载两个同名的 PayService.class

  • 双亲委派:向上委托父类加载器处理,确保核心 API(如 String)的安全与唯一。
  • 破坏手段:继承 ClassLoader重写 loadClass() 方法(绕过向上委托逻辑)。
  • 实战场景
    1. Tomcat 隔离:不同 Web 应用依赖不同版本的第三方库。
    2. 热部署:不重启应用,丢弃旧加载器,创建新加载器重新加载代码。
    3. SPI 机制:如 JDBC 驱动加载,利用线程上下文类加载器实现逆向调用。

4. 线上救火:OOM 与 Heap Dump 排查

面试官:线上支付接口变慢,Full GC 频繁,你怎么查?

  • 第一步:监控确认:使用 jstat -gcutil [pid] 1000 观察各区占用、GC 频率及耗时。
  • 第二步:导出快照
    • 自动触发:配置 -XX:+HeapDumpOnOutOfMemoryError
    • 手动抓取:使用 jmap -dump:format=b,file=heap.hprof [pid]
  • 第三步:MAT 分析
    1. Dominator Tree:找占用内存最大的对象。
    2. Path to GC Roots:看谁在引用这个垃圾(解决内存泄漏)。
  • 实战案例“曾排查过因一次性 SELECT 全量对账数据导致 Survivor 区溢出,对象直晋老年代引发频繁 Full GC 的问题,通过优化为分片(Batch)处理后解决。”

5. 栈溢出:StackOverflowError 的“深度”陷阱

面试官:递归写错报 StackOverflowError 怎么办?

  • 根因:线程私有的 虚拟机栈 空间固定(默认 1MB),方法调用过深(死循环递归)导致栈帧堆叠撑爆栈空间。
  • 排查:直接使用 jstack 打印线程堆栈,定位不断重复出现的代码行号。
  • 调优思路
    1. 优化递归逻辑(改为循环)。
    2. 若业务确实需要深层调用,调大 -Xss 参数。
    3. 若因“线程开得太多”报 OOM,应 减小 -Xss 以容纳更多线程。

💡 李工总结口诀:

“堆溢出看引用(MAT),栈溢出看行数(jstack),高频率看 GC(jstat)。”