第七部分:JVM 与性能优化(第 71~75 题)
进入「第三阶段架构师能力」。聚焦 Full GC、内存泄漏、CPU 100%、OOM、GC 调优。每题需给出:现象→诊断工具→根因→参数/代码修复→预防。
第 71 题:Full GC 频繁与 STW 治理 GC
- 1. Full GC 触发原因?2. 怎么诊断?3. 老年代涨满?4. 大对象/内存碎片?5. 参数怎么调?
第一步:分析。Full GC=全局回收,STW 长。常见原因:老年代满、元空间满、显式 System.gc、晋升失败、大对象直入老年代。
第二步:挑战。定位、晋升、碎片、参数。
第三步:架构。①诊断:GC 日志(-Xlog:gc*)+ jstat + dump 分析;②根因:老年代涨满→存在长期对象/缓存无界;③修复:限缓存大小(弱引用/LRU)、大对象分批、调 Survivor/晋升阈值;④选型:G1/ZGC 降 STW。
第四步:选型。G1(默认,Region)/ ZGC(亚毫秒 STW,大堆)。
第五步:一致性。GC 调优不影响语义;降 STW 保延迟。
第六步:高可用。避免 Full GC 致雪崩;HPA 按延迟。
第七步:优化。对象生命周期管理;避免 System.gc。
触发:老年代满触发 Full GC( serial old 或 G1 的 mixed/full)。无界缓存:最常见——本地 Map 一直 put 不淘汰,老年代涨满。大对象:超过 Region 50% 直入老年代,碎成 Full GC。G1/ZGC:G1 增量回收 Region,ZGC 并发几乎无 STW,适合低延迟大堆。
// 参数: G1 + GC日志 -Xms4g -Xmx4g -XX:+UseG1GC -Xlog:gc*:time,level,tags:file=gc.log:time // 修复无界缓存 → Caffeine LRU Cache<K,V> c = Caffeine.newBuilder().maximumSize(10000).build(); // 避免: System.gc(); 大对象分批处理
追问 1:G1 还 Full GC?G1 也有 Full GC(晋升失败/混合回收不及时),调 Region/并发线程;或换 ZGC。
追问 2:元空间满?动态生成类(反射/CGLIB)膨胀,调 -XX:MaxMetaspaceSize + 查类加载泄漏。
追问 3:碎片导致?老年代碎片→晋升失败→Full GC;G1 紧凑化缓解。
追问 4:ZGC 适用?大堆(>16G)+ 极低延迟(STW <10ms)首选 ZGC;小堆 G1 够。
追问 5:诊断工具?GC 日志 + jstat -gc + jmap dump + MAT/VisualVM 分析。
- ❌ 只加内存——掩盖问题。正确:定位根因。
- ❌ 无界缓存——老年代满。正确:LRU/限大小。
- ❌ 用 System.gc——强制 Full GC。正确:避免。
第 72 题:内存泄漏定位与修复 泄漏
ThreadLocal 持有大对象未清理,或静态 Map 缓存无限增长。要求:定位并根治内存泄漏。- 1. 泄漏 vs 正常增长?2. 怎么 dump 分析?3. ThreadLocal 泄漏?4. 静态集合?5. 预防?
第一步:分析。泄漏=对象不再用但被引用无法回收,堆持续涨。常见:ThreadLocal 未 remove、静态集合、未关资源(连接/流)、监听器。
第二步:挑战。定位、根因、修复、预防。
第三步:架构。①监控:内存曲线 + Full GC 频率;②dump:jmap -dump 或 OOM 自动 dump;③分析:MAT 看支配树(谁持有大对象);④修复:ThreadLocal 用 try-finally remove、缓存加上限/弱引用、资源 try-with-resources;⑤预防:OOM 自动 dump + 压测长稳。
第四步:选型。MAT / VisualVM + JVM 参数(HeapDumpOnOutOfMemoryError)。
第五步:一致性。泄漏修复不影响语义;降内存保稳定。
第六步:高可用。OOM 自动 dump 不重启丢现场;HPA 兜底。
第七步:优化。对象池复用;监控长稳。
ThreadLocal 泄漏:线程池线程复用,ThreadLocal 不被 GC(线程存活),必须 finally 中 remove,否则每次任务累积大对象。静态集合:static Map 永不回收,需 LRU/弱引用/定时清理。资源未关:b>连接/流/会话持有堆外或堆内对象,用 try-with-resources 保关闭。MAT 支配树:找「被谁引用而活」,直接定位根因。
// ThreadLocal 必须 remove private static final ThreadLocal<BigObj> TL = new ThreadLocal<>(); try { TL.set(load()); doWork(); } finally { TL.remove(); } // 防线程池累积泄漏 // JVM 参数: OOM 自动 dump -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/heap.hprof // 静态缓存: 用 WeakHashMap 或 Caffeine 上限
追问 1:堆外泄漏?堆外(Netty DirectBuffer/Native)jmap 看不到,用 NMT/pmap + -XX:NativeMemoryTracking。
追问 2:无法复现?长稳压测(如 72h)+ 内存监控曲线;灰度观察。
追问 3:MAT 看不懂?支配树(Dominator Tree)找 Retained Heap 最大且异常的对象。
追问 4:连接泄漏?连接池泄漏(未 close),监控活跃连接数;try-with-resources。
追问 5:防止线上泄漏?压测门禁 + 长稳测试 + OOM 自动 dump + 监控告警。
- ❌ ThreadLocal 不 remove——线程池泄漏。正确:finally remove。
- ❌ 静态 Map 无限——泄漏。正确:上限/弱引用。
- ❌ 资源不关——泄漏。正确:try-with-resources。
第 73 题:CPU 100% 与死循环/锁竞争 CPU
- 1. 怎么定位高 CPU 线程?2. 正则回溯?3. 锁竞争/自旋?4. 频繁 GC 也吃 CPU?5. 预防?
第一步:分析。CPU 100% 常因:死循环、正则回溯、锁竞争/自旋、频繁 GC、序列化过载。
第二步:挑战。定位线程、根因、修复。
第三步:架构。①定位:top -Hp pid 找高 CPU 线程 → jstack 看栈;②正则:用线性复杂度正则/预编译,避免 (a+)+$ 回溯;③锁:自旋/ CAS 改公平锁/减小临界区;④GC:频繁 Full GC 占 CPU,先治 GC;⑤预防:压测 + CPU 监控。
第四步:选型。top/jstack/arthas + 火焰图(async-profiler)。
第五步:一致性。修复不影响语义;降 CPU 保吞吐。
第六步:高可用。限流/降级防 CPU 打满雪崩。
第七步:优化。热点代码火焰图定位;算法优化。
定位套路:top 找进程→top -Hp 找线程→换算 16 进制→jstack 匹配 nid。正则回溯:嵌套量词 (a+)+b 对长串匹配失败会指数回溯,改用 possessive/原子组或线性写法。自旋锁:while(CAS) 空转吃满 CPU,改 Lock(park)或限自旋次数。火焰图:async-profiler 采样,直观看 CPU 花在哪。
// 正则防回溯: 用 possessive (++) 或预编译+线性 Pattern p = Pattern.compile("a++b"); // 贪婪占有, 不回溯 // 自旋改 Lock private final Lock lock = new ReentrantLock(); lock.lock(); try { critical(); } finally { lock.unlock(); } // 诊断: top -Hp <pid> ; jstack <pid> | grep -A 20 <nid>
追问 1:arthas 怎么用?thread -n 3 看最忙线程栈;trace/watch 定位方法耗时。
追问 2:GC 占 CPU?频繁 Full GC 占 CPU,先调 GC(Q71);jstat 看 GC 时间。
追问 3:序列化吃 CPU?大对象频繁序列化(JSON),换高效(protobuf)或缓存序列化结果。
追问 4:火焰图怎么做?async-profiler -d 30 -f flamegraph.html pid。
追问 5:预防高 CPU?压测 + CPU 监控 + 正则/锁 Code Review 规则。
- ❌ 正则嵌套量词——回溯爆 CPU。正确:线性/possessive。
- ❌ 自旋空转——吃满 CPU。正确:Lock/park。
- ❌ 不查 GC——漏因。正确:jstat 看 GC 时间。
第 74 题:OOM 全景与各类溢出 OOM
Java heap space、Metaspace、Direct buffer、Unable to create new native thread。要求:区分类型并分别给出治理方案。- 1. 各类 OOM 区别?2. 堆外 OOM?3. 线程数 OOM?4. 元空间 OOM?5. 预防?
第一步:分析。OOM 不是一种:堆、元空间、直接内存、线程栈各有根因,必须分而治之。
第二步:挑战。类型区分、根因、修复。
第三步:架构。①heap:内存泄漏/堆太小→dump+MAT(Q72);②Metaspace:类加载泄漏(CGLIB/反射)→限制 + 查卸载;③Direct buffer:Netty 直接内存未释放/超上限→调 -XX:MaxDirectMemorySize + 池化;④unable create thread:线程数超限(线程池失控/栈过大)→限线程池 + 降栈;⑤预防:自动 dump + 监控。
第四步:选型。JVM 参数 + NMT(Native 跟踪)+ MAT。
第五步:一致性。修复不影响语义;降内存保稳定。
第六步:高可用。OOM 自动 dump 留现场;HPA 兜底。
第七步:优化。对象池/线程池上限;监控。
堆 OOM:最常见,泄漏或堆设小。元空间:动态类(CGLIB 代理、Groovy、反射生成)不断生成不卸载,调 MaxMetaspaceSize + 查类加载器泄漏。Direct buffer:Netty 默认用堆外,未 release() 或超 MaxDirectMemorySize→OOM;用池化 ByteBuf + 显式 release。线程 OOM:每线程占栈(默认 1M),线程池无上限→线程爆→无法建线程;限池大小 + 降 -Xss。
// 关键参数 -Xmx4g -XX:MaxMetaspaceSize=256m -XX:MaxDirectMemorySize=1g -Xss256k -XX:+HeapDumpOnOutOfMemoryError // Netty 直接内存: 池化 + release ByteBuf buf = PooledByteBufAllocator.DEFAULT.directBuffer(n); try { ... } finally { buf.release(); } // 线程池必须上限 new ThreadPoolExecutor(..., new LinkedBlockingQueue<>(1000), reject);
追问 1:直接内存看不到?jmap 只堆;用 NMT(-XX:NativeMemoryTracking=detail)或 pmap 看。
追问 2:线程 OOM 但内存够?线程栈占虚拟内存/进程线程数上限(ulimit -u);限池+降 Xss。
追问 3:元空间不回收?类加载器泄漏(web 热部署常见);重启类加载器需释放引用。
追问 4:OOM 自动 dump 影响?dump 时 STW 短暂,但留现场值得;生产开。
追问 5:预防?监控各内存区 + 压测长稳 + 池上限 + 自动 dump。
- ❌ 所有 OOM 只加堆——其他区无效。正确:分区治。
- ❌ Netty 不 release——堆外泄漏。正确:池化+release。
- ❌ 线程池无上限——线程 OOM。正确:限大小。
第 75 题:GC 选型与参数调优 GC调优
- 1. 各 GC 适用?2. 吞吐 vs 延迟?3. 参数怎么调?4. 大堆选谁?5. 调优方法论?
第一步:分析。GC 选型看目标:吞吐(Parallel)还是延迟(G1/ZGC)。无银弹。
第二步:挑战。目标权衡、参数、大堆。
第三步:架构。①Parallel:高吞吐、可接受长 STW(批处理);②G1:平衡(默认,<=数十 G,可控停顿);③ZGC/Shenandoah:大堆亚毫秒 STW(低延迟服务);④调优:设堆大小(Xms=Xmx 防抖动)、Region、并发线程;⑤方法:先测基线→定位瓶颈→调参→验证。
第四步:选型。批处理 Parallel;普通服务 G1;低延迟大堆 ZGC。
第五步:一致性。GC 不影响语义;调优降 STW。
第六步:高可用。低延迟 GC 防超时雪崩。
第七步:优化。对象生命周期短(Young 多)= GC 友好;避免大对象。
Parallel:多线程吞吐最高,但 Full GC STW 长,适合离线批处理。G1:Region 化增量回收,停顿目标可设(-XX:MaxGCPauseMillis),默认首选。ZGC:并发标记/转移,STW <10ms 且不随堆增长,适合 8G~数 T 低延迟。Xms=Xmx:避免堆动态扩缩抖动;调优先测:别盲调,靠 GC 日志 + 压测数据。
// 交易服务(平衡): G1 -Xms8g -Xmx8g -XX:+UseG1GC -XX:MaxGCPauseMillis=200 // 低延迟大堆: ZGC -Xms32g -Xmx32g -XX:+UseZGC -XX:ZCollectionInterval=... // 批处理: Parallel -XX:+UseParallelGC // 验证: -Xlog:gc*:file=gc.log + 压测对比 P99
追问 1:ZGC 代价?吞吐略低于 G1(并发开销),但延迟极优;大堆首选。
追问 2:G1 停顿不达标?调 MaxGCPauseMillis(目标非保证)+ 增 Region/并发线程;仍不行换 ZGC。
追问 3:堆越大越好?堆大=GC 停顿长(除 ZGC);按对象存活率定,避免无谓大堆。
追问 4:调优顺序?基线压测→GC 日志→定位(Young/Full)→针对性调→再验证。
追问 5:容器内存?设 Xmx < 容器 limit(留元空间/栈/堆外),防被 OOMKill。
- ❌ 一律 Parallel——延迟差。正确:按目标选。
- ❌ Xms≠Xmx——抖动。正确:相等。
- ❌ 盲调参数——无依据。正确:实测驱动。