波克二面:JVM、OOM 排查与用 Agent 做监控分析
- 轮次
- 二面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 自我介绍。
- 介绍一下 JVM。
- 了解 Java 垃圾回收机制吗?介绍一下。
- CMS 怎么找到那些需要回收的对象?
- 程序出现了 OOM 如何排查?
- 你说查看 jstat 时如果老年代比例下降是什么意思?不下降又是什么意思?
- 你遇到的真实 OOM 情况是为什么?List 里存了很多对象、ArrayList 不再使用了,但里面的对象一直被引用,这算内存泄漏对吗?
- OOM 能用 AI 分析出来吗?怎么做?
- 设计一个 Agent 专门用来做 OOM 或其他问题的监控指标排查,你会怎么设计?
- Agent 在使用设计好的工具调用时有没有可能出现幻觉?一直认为是某一类 OOM、调用重复工具怎么解决?
- 最近两个月遇到最难的问题是什么?
- 你每天开发使用 AI 大概用多少 Token?
《参考解析》
CMS 怎么找到需要回收的对象:前提是 JVM 用可达性分析而不是引用计数——从 GC Roots(虚拟机栈里的局部变量、方法区静态变量与常量引用、JNI 引用、活跃线程等)出发遍历引用链,不可达的对象才判定可回收。CMS 的难点在于「并发标记」:标记阶段用户线程还在跑,会不断产生新的引用关系,如果不管,就可能把「并发期间新引用上的对象」漏标成垃圾(三色标记里的漏标问题)。CMS 用增量更新(incremental update)+ 写屏障解决:并发标记期间,凡是把引用从黑对象指向白对象(新挂上引用)的写操作,都通过写屏障记录下来,重新标记(remark)阶段再以这些记录为根重新扫一遍,所以 remark 虽然会 STW 但很短。可以顺手对比 G1 用的 SATB(原始快照):它记录的是「引用被删除前的旧值」,关注的是并发期间断开的引用,二者解决的是同一个漏标问题的两个方向。常见的追问是:为什么 remark 比 initial mark 慢(要处理写屏障队列和整个堆的残留标记)、并发清理阶段新产生的垃圾怎么办(浮动垃圾,留到下次收集)、以及 CMS 的碎片问题(标记清除不整理,碎片多了会退化成 Serial Old 单线程 Full GC)。
OOM 的排查路径:先分类,再取证。分类靠异常信息与监控:Java heap space 是堆,Metaspace 是类元数据(多半是动态代理/热部署类加载器泄漏),GC overhead limit exceeded 是回收效率过低,unable to create new native thread 是线程数触顶,还有直接内存溢出。取证工具链是固定的:jstat -gcutil <pid> 1000 看各区占用、GC 次数与耗时;jmap -histo:live 看对象直方图找到数量或体积异常增长的类;提前配好 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=... 拿到 dump,用 MAT 或 JProfiler 看支配树(dominator tree)和「Path to GC Roots」,看是谁在持有它;线上不便 dump 时可以用 jcmd 触发、或先看 NMT 判断是否堆外。定位到持有者后,再区分是「内存泄漏」还是「内存不足」:泄漏是对象本该回收却仍被引用,典型来源是静态集合当缓存、监听器与回调没注销、ThreadLocal 用完没 remove(线程池里尤其致命)、连接与流没关、缓存没有容量上限。题目里的例子就是标准泄漏——ArrayList 这个容器虽然不再使用了,但只要它还被某个长期存活的对象(静态变量、单例里的字段、还没结束的 ThreadLocal)引用着,里面的元素就都不可回收,容器本身也就一起留着。最后一步是修:有界缓存加淘汰策略、用弱引用、把 ThreadLocal 放进 finally 清理,并用压测或长稳回归验证不再增长。
jstat 老年代占比变化怎么读:要结合绝对值和趋势一起看。每次 Full GC 之后老年代使用量下降,说明这一轮确实回收掉了对象,属于正常涨落——业务在跑,对象在晋升、被回收,占比呈锯齿形波动是健康的。如果连续几次 Full GC 之后老年代占用不降反升(面试官说的「不下降」),说明对象在被持续晋升却回收不掉,两种可能:一是参数不合理,新生代太小或晋升阈值(-XX:MaxTenuringThreshold、动态年龄判定)导致短命对象过早进老年代,表现为「GC 频繁但不泄漏」,调大新生代或换 G1 观察是否缓解;二是真的在泄漏,对象被长期引用,只能靠 dump 找引用链。判断分水岭是「占用是否持续单调上升、Full GC 频率是否越来越密、单次 Full GC 后回收量是否越来越小」,三条同时成立基本可以判定泄漏。补充一句:占比高但稳定、GC 耗时可控,通常不是问题(很多服务常年 70% 以上),不要只凭一个百分比下结论。
设计一个做 OOM 排查的 Agent,以及怎么抑制它的幻觉:核心思路是把「确定性的事情交给程序,判断交给模型」。工具集按排查路径配:拉取 GC 与内存指标(jstat/JMX/监控系统)、拉对象直方图 topN、拉 dump 的摘要(支配树前 N 项、增长最快的类、到 GC Roots 的引用链)、查最近的发布与配置变更、检索同类历史工单。绝对不要把原始 dump 直接喂给模型——几十 GB 的数据既是 token 灾难也没必要,先在工具侧做确定性预处理,只把结构化摘要送进上下文。流程上做成状态机而不是自由对话:第一步分流(堆 / 元空间 / 直接内存 / 线程),第二步在分支内按清单取证,每步记录「已获得哪些证据、排除了哪些假设」,最后输出结论 + 证据引用 + 建议动作。抑制幻觉有五个抓手:① 每个结论必须绑定具体证据(工具返回的字段与时间点),没有证据不许下结论,输出里强制带上引用;② 能用规则判定的前置事实就用解析器判定(比如「Full GC 后老年代不降」由 jstat 解析器给出布尔值),不让模型去猜数字;③ 重复调用检测:对「工具名 + 参数」做哈希,命中已执行过的调用就拒绝并提示模型换策略,同时把已排除的假设写进状态,避免它在同一个方向上打转;④ 明确的停止条件与置信度,证据不足时输出「需要人工介入 + 建议补充哪个指标」,而不是硬给一个分类;⑤ 用积累了标注结论的历史 case 做回归评测,每次改 prompt 或换模型都跑一遍,用数据决定改动是否有效。这套做法同样适用于其他线上问题的自动排查,区别只是工具集和分流树。