线上 OOM 怎么定位:Java 原生命令与 Arthas 两套思路
本页用于快速回答场景题。如果你需要按事故时间线执行、理解 MAT 每一步、区分泄漏与峰值,并覆盖 JDK 7、JDK 8、JDK 9+ 和容器排查,请继续阅读 OOM 生产级排查手册。性能图概念见 火山图、火焰图与 MAT 辨析。
这是一道典型线上场景题。回答时不能只说“看 dump、调大内存”。合格答案要能说明:先保现场,再判断 OOM 类型,再选择低风险命令采证,再定位对象或资源来源,最后给出修复、验证和预防措施。
一句话标准思路:
OOM 不是单一问题,而是内存结果。先判断是 Java 堆、元空间、直接内存、线程、本地内存还是容器 OOMKilled,再用 GC 日志、线程栈、对象直方图、heap dump、NMT、Arthas 等工具定位“谁占内存、为什么释放不掉、峰值为什么超预算”。
面试回答总流程
flowchart TD
A["发现线上 OOM / 容器重启"] --> B["先保现场"]
B --> C["确认进程是否还活着"]
C --> D["确认 OOM 类型"]
D --> E["低风险采集: 日志、GC、线程栈、对象直方图"]
E --> F{"是否需要 heap dump"}
F -- "需要" --> G["确认磁盘和停顿风险后导出"]
F -- "不需要" --> H["先用直方图和监控缩小范围"]
G --> I["MAT / Arthas 分析大对象和引用链"]
H --> I
I --> J["定位代码根因"]
J --> K["修复、压测、加监控和容量边界"]线上第一原则:不要一上来重启,也不要一上来 dump。重启会丢现场;dump 可能导致长时间 STW、磁盘打满、服务雪上加霜。先确认影响范围、是否有副本、是否能摘流量。
先分成两种现场:系统崩溃还是仍在运行
OOM 后的第一处分支不是立即选 jmap 还是 Arthas,而是确认原 Java 进程是否还存在。
| 现场 | 排查能力 | 第一批动作 |
|---|---|---|
| 系统已经崩溃或容器已重启 | 无法再读取原进程的实时线程、对象和内存 | 查自动 hprof、GC 日志、应用日志、监控、Pod previous log、事件、退出码、hs_err |
| 系统仍在运行 | 可以用 JDK 命令或 Arthas 在线采证 | 判断是否假存活,有副本先摘流量,再采参数、GC、线程栈和直方图 |
OutOfMemoryError 不保证 JVM 一定退出。它可能只让当前请求或任务线程失败,进程仍然存在;但系统可能正在 Full GC、线程池堆积,属于“进程活着、业务濒死”。反过来,容器超过 cgroup limit 时可能直接被内核杀掉,只看到 OOMKilled 和退出码 137,没有 Java OOM 异常栈。
两套完整操作手册见:
如果你现在面对的是一条真实告警,不知道先登录哪里、PID 怎么找、日志在哪、该搜索什么关键词,请直接从 从告警开始:第一分钟到底做什么 开始,随后按实际部署环境进入 Kubernetes、Docker、Linux/systemd 或 Windows 的取证步骤。
第一步:先判断是哪类 OOM
不同 OOM 的排查方向完全不同。
| 错误或现象 | 典型文本 | 优先方向 |
|---|---|---|
| 堆 OOM | java.lang.OutOfMemoryError: Java heap space | 对象太多、大对象、缓存、集合、队列、ThreadLocal |
| GC overhead | GC overhead limit exceeded | Full GC 频繁但回收很少,基本按堆 OOM 查 |
| 元空间 OOM | OutOfMemoryError: Metaspace,JDK7 是 PermGen space | 动态代理、CGLIB、脚本、热部署、ClassLoader 泄漏 |
| 直接内存 OOM | OutOfMemoryError: Direct buffer memory | NIO、Netty、DirectByteBuffer、ByteBuf 未释放 |
| 线程 OOM | unable to create native thread | 线程数、线程池、-Xss、OS 限制、容器内存 |
| 数组过大 | Requested array size exceeds VM limit | 一次性加载大文件、大集合、大分页 |
| 容器杀死 | OOMKilled、退出码 137 | 进程总内存超过 cgroup limit,不一定有 Java OOM 栈 |
判断依据:
- 应用日志里有没有
OutOfMemoryError。 - GC 日志里 OOM 前是否 Full GC 频繁。
- 容器平台是否显示
OOMKilled。 - 进程是否还在。进程不在时,只能看 previous log、事件、dump 文件、监控。
第二步:Java 原生命令排查思路
Java 原生命令适合不依赖外部工具的标准排查,面试和生产都必须会。
1. 找到进程和启动参数
jps -l
jcmd <pid> VM.flags
jcmd <pid> VM.command_line
jcmd <pid> VM.system_properties看什么:
| 信息 | 目的 |
|---|---|
-Xms、-Xmx | 堆上限是否合理 |
MaxMetaspaceSize | 元空间是否有限制 |
MaxDirectMemorySize | 直接内存是否有限制 |
Xss | 每个线程栈大小 |
| GC 类型 | G1、CMS、Parallel 等,决定日志解读方式 |
| HeapDump 参数 | OOM 时是否自动留 dump |
建议线上默认配置:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dumpJDK 8 GC 日志示例:
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/data/logs/gc.logJDK 9+ GC 日志示例:
-Xlog:gc*,safepoint:file=/data/logs/gc.log:time,uptime,level,tags2. 看 GC 趋势
jstat -gcutil <pid> 1000 10
jstat -gccapacity <pid> 1000 5重点看:
| 指标 | 判断 |
|---|---|
OU | 老年代使用率是否持续接近 100% |
FGC | Full GC 次数是否快速增长 |
FGCT | Full GC 总耗时是否很大 |
YGC | Young GC 是否异常频繁 |
| Full GC 后 old 区是否下降 | 如果降不下来,说明大量对象仍存活 |
如果 Full GC 后老年代仍然很高,通常不是“GC 不工作”,而是对象还被引用,或者业务峰值超过堆容量。
3. 先抓线程栈
jcmd <pid> Thread.print > /tmp/app-thread-1.txt
sleep 2
jcmd <pid> Thread.print > /tmp/app-thread-2.txt
sleep 2
jcmd <pid> Thread.print > /tmp/app-thread-3.txt线程栈用于判断:
- 是否有大量业务线程卡住。
- 是否有大量线程等待数据库连接、HTTP 下游、MQ、锁。
- 是否因为线程池堆积导致任务对象堆积。
- 线程 OOM 时,到底创建了哪些线程。
如果是 unable to create native thread,还要看:
ps -eLf | grep java | wc -l
ulimit -u
jcmd <pid> VM.flags | grep ThreadStackSize4. 看对象直方图
相对 heap dump,直方图成本更低,适合先粗定位。
jcmd <pid> GC.class_histogram > /tmp/histo.txt或者:
jmap -histo:live <pid> > /tmp/histo-live.txt注意:jmap -histo:live 会触发 Full GC,生产上要谨慎。可以优先使用 jcmd GC.class_histogram,再结合现场决定是否抓 live。
直方图看什么:
| 现象 | 可能根因 |
|---|---|
byte[]、char[] 很大 | 大字符串、大文件、序列化数据、缓存 |
java.util.HashMap$Node 很多 | Map 缓存、ThreadLocalMap、业务集合 |
| 业务 DTO 数量巨大 | 查询无分页、批处理全量加载、MQ 堆积 |
LinkedBlockingQueue$Node 很多 | 线程池或业务队列无界堆积 |
Thread 很多 | 线程泄漏、无限创建线程 |
Class、ClassLoader 相关很多 | Metaspace 或类加载器问题 |
5. 必要时导出 heap dump
导出前必须确认:
- 磁盘空间足够,dump 文件可能接近堆大小。
- 当前实例能否摘流量。
- 是否有副本可以承接流量。
- 是否已经保留应用日志、GC 日志、线程栈。
命令:
jcmd <pid> GC.heap_dump /data/dump/app-oom.hprof或:
jmap -dump:format=b,file=/data/dump/app-oom.hprof <pid>MAT 分析重点:
| 功能 | 看什么 |
|---|---|
| Leak Suspects | 初步怀疑点,不要盲信 |
| Histogram | 哪些类实例多、浅堆大 |
| Dominator Tree | 谁支配的 Retained Heap 最大 |
| Path To GC Roots | 对象为什么还活着 |
| Thread Overview | 是否线程栈持有大对象 |
结论必须落到引用链。例如:
heap dump 中 OrderExportTask 持有 ArrayList,Retained Heap 1.6GB。
引用链为 Thread -> ExportService.exportAll -> ArrayList -> OrderDTO[]。
根因是导出接口一次性查询并暂存 300 万条订单,没有分页和流式写出。6. 直接内存和本地内存
如果是 Direct Memory、Metaspace、容器 OOMKilled,只看 heap dump 不够。
需要启动时开启 NMT:
-XX:NativeMemoryTracking=summary查看:
jcmd <pid> VM.native_memory summary看什么:
| 区域 | 含义 |
|---|---|
| Java Heap | Java 堆 |
| Class | 类元数据,和 Metaspace 相关 |
| Thread | 线程栈和线程本地结构 |
| Code | JIT 编译代码缓存 |
| GC | GC 自身本地结构 |
| Internal / Symbol / Arena | JVM 内部结构 |
容器里要核算总预算:
容器 limit > Xmx + MaxMetaspaceSize + MaxDirectMemorySize + 线程数 * Xss + CodeCache + GC/JVM 开销第三步:Arthas 排查思路
Arthas 适合进程还活着时在线诊断。它的优势是交互式强,可以快速看 JVM、线程、对象、调用栈、运行时参数;缺点是不能替代离线 MAT,某些命令也有性能风险。
1. 启动和连接
java -jar arthas-boot.jar选择目标进程后进入 Arthas 控制台。
如果服务器不能外网下载,生产应提前准备 arthas 包,并按公司安全规范执行。不要在未授权环境随意 attach。
2. 先看基础信息
dashboard
jvm
sysprop
sysenv
vmoption看什么:
| 命令 | 作用 |
|---|---|
dashboard | CPU、内存、GC、线程概览 |
jvm | JVM 内存、GC、线程、类加载信息 |
vmoption | 查看 JVM 参数 |
sysprop | 系统属性 |
sysenv | 环境变量 |
如果 dashboard 中 old 区持续很高,Full GC 频繁,基本进入堆 OOM 排查。
3. 用 thread 看线程问题
thread
thread -n 10
thread --state BLOCKED
thread <threadId>用途:
- 找 CPU 高线程。
- 找大量 BLOCKED 线程。
- 找卡在数据库、HTTP、MQ、锁的线程。
- 判断是否线程池堆积。
如果 OOM 是队列堆积引起,线程栈常能看到消费者卡在下游,例如 JDBC、HTTP client、Redis、MQ ack 等。
4. 用 memory 和 heapdump 看内存
Arthas 可用:
memory
heapdump /data/dump/arthas-heap.hprofmemory 用于看堆、非堆、直接内存等概况。heapdump 会导出堆快照,风险和 jmap 类似,仍要确认磁盘和停顿影响。
如果只需要快速看对象分布,可以用:
heapdump --live /data/dump/arthas-live.hprof注意:--live 会触发 Full GC,生产谨慎使用。
5. 用 vmtool 查看对象实例
vmtool 可以直接从 JVM 中查询某个类的实例,适合已经怀疑某个类泄漏时验证。
示例:查看某个业务对象实例数量。
vmtool --action getInstances --className com.example.order.OrderDTO --limit 10查看对象字段:
vmtool --action getInstances --className com.example.order.OrderDTO --express 'instances[0]'风险:不要对实例数量巨大、对象复杂的类随意查询太多实例。limit 必须控制。
6. 用 ognl 看静态缓存
如果怀疑本地静态缓存泄漏,可以用 ognl 看静态字段。
ognl '@com.example.cache.LocalCache@CACHE.size()'如果是 Caffeine、Guava Cache、本地 Map,可以看 size、key 分布、最大容量配置。
注意:ognl 能执行代码,有风险。生产只做只读表达式,不要调用清理、删除、写入等有副作用的方法,除非有明确应急审批。
7. 用 watch/trace 定位增长入口
如果内存还在持续上涨,要找是谁不断往集合、缓存、队列里放数据。
示例:观察缓存 put。
watch com.example.cache.LocalCache put '{params, returnObj}' -n 5 -x 2观察导出接口返回:
trace com.example.export.ExportService exportAll '#cost > 1000'观察某方法入参大小:
watch com.example.importer.ImportService importBatch '{params[0].size()}' -n 10风险控制:
| 命令 | 风险 | 控制 |
|---|---|---|
watch | 高频方法会产生大量输出 | 加 -n、条件表达式、限制展开层级 |
trace | 高频链路有性能开销 | 只追低频或加耗时条件 |
tt | 记录调用现场,占内存 | 生产谨慎使用,及时清理 |
ognl | 可执行表达式 | 只读、限权、不要改状态 |
Java 原生命令和 Arthas 怎么选
| 场景 | 优先工具 |
|---|---|
| 进程已经挂了 | 日志、GC 日志、heap dump、容器事件 |
| 进程还活着,需要标准证据 | jcmd、jstat、jstack、jmap |
| 需要交互式快速观察 | Arthas dashboard、jvm、thread、memory |
| 已怀疑某个类或缓存 | Arthas vmtool、ognl |
| 需要离线分析引用链 | heap dump + MAT |
| 容器 OOMKilled | Kubernetes 事件、previous logs、NMT、内存预算 |
结论:Arthas 不是替代 Java 原生命令,而是在线诊断增强。最终证据仍然要能落到 dump、日志、引用链、代码位置和监控数据上。
典型场景一:接口导出导致 Heap OOM
现象:
java.lang.OutOfMemoryError: Java heap space错误代码:
public List<OrderDTO> exportAll() {
List<OrderDTO> orders = orderMapper.selectAll();
return orders;
}Java 原生命令思路:
jstat -gcutil <pid> 1000 10
jcmd <pid> GC.class_histogram > /tmp/histo.txt
jcmd <pid> Thread.print > /tmp/thread.txt
jcmd <pid> GC.heap_dump /data/dump/export-oom.hprofMAT 里如果看到 ArrayList、OrderDTO、byte[]、char[] 占用大,并且引用链来自导出线程,就能定位为全量导出峰值过高。
Arthas 思路:
dashboard
jvm
thread -n 10
memory
trace com.example.export.OrderExportService exportAll '#cost > 1000'
watch com.example.export.OrderExportService exportAll '{returnObj == null ? null : returnObj.size()}' -n 3修复:
- 分页查询。
- 流式写 Excel/CSV。
- 限制最大导出范围。
- 大导出改异步任务。
- 导出文件落对象存储,前端轮询下载。
典型场景二:本地缓存无上限
错误代码:
public class LocalCache {
public static final Map<String, Object> CACHE = new ConcurrentHashMap<>();
public static void put(String key, Object value) {
CACHE.put(key, value);
}
}根因:静态 Map 是 GC Roots 可达路径上的对象,key 不删除,value 就不能回收。
Java 原生命令:
jcmd <pid> GC.class_histogram > /tmp/histo.txt
jcmd <pid> GC.heap_dump /data/dump/cache-oom.hprofMAT 看:
System Class -> LocalCache -> CACHE -> ConcurrentHashMap -> Node[] -> valueArthas:
ognl '@com.example.cache.LocalCache@CACHE.size()'
watch com.example.cache.LocalCache put '{params[0], params[1]}' -n 10 -x 1修复:
- 使用 Caffeine,设置
maximumSize和过期时间。 - key 设计包含租户和版本,避免无限增长。
- 监控 cache size、命中率、淘汰数。
- 大对象不要放本地缓存。
典型场景三:ThreadLocal 泄漏
错误代码:
public class UserContextHolder {
private static final ThreadLocal<UserContext> LOCAL = new ThreadLocal<>();
public static void set(UserContext context) {
LOCAL.set(context);
}
}如果请求结束不 remove,线程池线程长期存活,value 可能一直挂在 ThreadLocalMap 上。
标准写法:
try {
UserContextHolder.set(context);
chain.doFilter(request, response);
} finally {
UserContextHolder.remove();
}Java 原生命令看 heap dump 引用链:
Thread -> ThreadLocalMap -> Entry -> value -> UserContextArthas 可以先看线程池线程和业务线程:
thread
thread --state WAITING如果已知 Holder 类,可以用 ognl 检查当前线程上下文,但这种问题通常最终仍要靠 heap dump 的引用链确认。
典型场景四:Direct Memory OOM
现象:
java.lang.OutOfMemoryError: Direct buffer memory常见于 Netty、NIO、直接缓冲区未释放。
Java 原生命令:
jcmd <pid> VM.flags
jcmd <pid> VM.native_memory summary
jcmd <pid> Thread.print > /tmp/thread.txt前提是启动时开启:
-XX:NativeMemoryTracking=summaryArthas:
memory
jvm如果是 Netty:
-Dio.netty.leakDetection.level=advanced修复:
- 检查
ByteBufretain/release。 - 不要把 DirectBuffer 长期放集合。
- 复用缓冲区。
- 合理设置
MaxDirectMemorySize。 - 容器内预留直接内存预算。
典型场景五:容器 OOMKilled
现象:
Kubernetes Last State: Terminated
Reason: OOMKilled
Exit Code: 137这不一定有 Java OOM 栈。
排查:
kubectl describe pod <pod>
kubectl logs <pod> --previous核算:
进程总内存 = 堆 + 元空间 + 直接内存 + 线程栈 + CodeCache + GC/JVM 本地结构 + JNI错误配置示例:
容器 limit 1024m
-Xmx900m
MaxDirectMemorySize 默认或较大
线程数 300,-Xss1m这类配置很容易超过 1GB 被杀。
修复:
- 降低
-Xmx,给堆外留空间。 - 限制直接内存和元空间。
- 收敛线程数。
- 提高容器 limit。
- 开启监控:RSS、heap、non-heap、direct、线程数、GC。
最后怎么写排查结论
面试或事故复盘要按证据链说,不要只说“内存不够”。
模板:
现象:
2026-07-12 10:20 服务出现 OutOfMemoryError: Java heap space,接口大量 500。
证据:
GC 日志显示 OOM 前 Full GC 连续触发,Old 区回收后仍保持 98%。
对象直方图显示 OrderDTO 和 ArrayList 数量异常。
heap dump 的 Dominator Tree 中 ExportTask 持有 ArrayList,占用 Retained Heap 1.6GB。
Path To GC Roots 显示引用链来自导出线程栈。
根因:
导出接口一次性查询 300 万条订单并放入 List,再生成 Excel,导致堆峰值超过 Xmx。
修复:
改为分页查询和流式写文件,限制单次导出时间范围,大导出改异步任务。
预防:
增加导出行数限制、堆使用率告警、GC 告警、OOM 自动 dump、压测覆盖大数据导出场景。面试标准回答
线上 OOM 怎么定位
我会先保现场,不会直接重启或直接调大 -Xmx。第一步看日志、GC 日志、容器事件,确认是 Java heap space、Metaspace、Direct buffer memory、unable to create native thread 还是容器 OOMKilled。第二步用 jstat 看 GC 趋势,用 jcmd Thread.print 抓线程栈,用 jcmd GC.class_histogram 看对象分布。必要时确认磁盘和停顿风险后用 jcmd GC.heap_dump 导出 dump,再用 MAT 看 Dominator Tree 和 Path To GC Roots。第三步根据引用链定位代码根因,比如无界集合、本地缓存、ThreadLocal、全量查询、MQ 堆积、大文件一次性加载。最后修代码、调容量、压测验证,并补监控和 OOM dump 参数。
用 Java 原生命令怎么查
先用 jps 或 jcmd 找 PID,用 jcmd VM.flags 看 JVM 参数,用 jstat -gcutil 看 GC 是否频繁和 Full GC 后老年代是否下降,用 jcmd Thread.print 连续抓线程栈,用 jcmd GC.class_histogram 看对象直方图。如果怀疑堆泄漏,确认风险后用 jcmd GC.heap_dump 导出堆并用 MAT 分析;如果是直接内存或容器 OOM,要结合 NMT、VM.native_memory、容器事件和进程总内存预算。
用 Arthas 怎么查
进程还活着时,可以 attach Arthas。先用 dashboard、jvm、memory 看堆、非堆、GC 和线程概况;用 thread -n、thread --state BLOCKED 看 CPU 高和阻塞线程;必要时用 heapdump 导出堆;如果怀疑某个类或缓存,用 vmtool --action getInstances 控制数量查看实例,用 ognl 只读查看静态缓存大小;如果内存持续增长,用 watch 或 trace 观察是谁不断写入缓存、集合或触发大查询。Arthas 命令要限制次数和条件,避免对高频方法造成额外压力。
