Skip to content

线上 OOM 怎么定位:Java 原生命令与 Arthas 两套思路

本页用于快速回答场景题。如果你需要按事故时间线执行、理解 MAT 每一步、区分泄漏与峰值,并覆盖 JDK 7、JDK 8、JDK 9+ 和容器排查,请继续阅读 OOM 生产级排查手册。性能图概念见 火山图、火焰图与 MAT 辨析

这是一道典型线上场景题。回答时不能只说“看 dump、调大内存”。合格答案要能说明:先保现场,再判断 OOM 类型,再选择低风险命令采证,再定位对象或资源来源,最后给出修复、验证和预防措施。

一句话标准思路:

OOM 不是单一问题,而是内存结果。先判断是 Java 堆、元空间、直接内存、线程、本地内存还是容器 OOMKilled,再用 GC 日志、线程栈、对象直方图、heap dump、NMT、Arthas 等工具定位“谁占内存、为什么释放不掉、峰值为什么超预算”。

面试回答总流程

mermaid
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 的排查方向完全不同。

错误或现象典型文本优先方向
堆 OOMjava.lang.OutOfMemoryError: Java heap space对象太多、大对象、缓存、集合、队列、ThreadLocal
GC overheadGC overhead limit exceededFull GC 频繁但回收很少,基本按堆 OOM 查
元空间 OOMOutOfMemoryError: Metaspace,JDK7 是 PermGen space动态代理、CGLIB、脚本、热部署、ClassLoader 泄漏
直接内存 OOMOutOfMemoryError: Direct buffer memoryNIO、Netty、DirectByteBuffer、ByteBuf 未释放
线程 OOMunable to create native thread线程数、线程池、-Xss、OS 限制、容器内存
数组过大Requested array size exceeds VM limit一次性加载大文件、大集合、大分页
容器杀死OOMKilled、退出码 137进程总内存超过 cgroup limit,不一定有 Java OOM 栈

判断依据:

  1. 应用日志里有没有 OutOfMemoryError
  2. GC 日志里 OOM 前是否 Full GC 频繁。
  3. 容器平台是否显示 OOMKilled
  4. 进程是否还在。进程不在时,只能看 previous log、事件、dump 文件、监控。

第二步:Java 原生命令排查思路

Java 原生命令适合不依赖外部工具的标准排查,面试和生产都必须会。

1. 找到进程和启动参数

bash
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

建议线上默认配置:

bash
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dump

JDK 8 GC 日志示例:

bash
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/data/logs/gc.log

JDK 9+ GC 日志示例:

bash
-Xlog:gc*,safepoint:file=/data/logs/gc.log:time,uptime,level,tags

2. 看 GC 趋势

bash
jstat -gcutil <pid> 1000 10
jstat -gccapacity <pid> 1000 5

重点看:

指标判断
OU老年代使用率是否持续接近 100%
FGCFull GC 次数是否快速增长
FGCTFull GC 总耗时是否很大
YGCYoung GC 是否异常频繁
Full GC 后 old 区是否下降如果降不下来,说明大量对象仍存活

如果 Full GC 后老年代仍然很高,通常不是“GC 不工作”,而是对象还被引用,或者业务峰值超过堆容量。

3. 先抓线程栈

bash
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,还要看:

bash
ps -eLf | grep java | wc -l
ulimit -u
jcmd <pid> VM.flags | grep ThreadStackSize

4. 看对象直方图

相对 heap dump,直方图成本更低,适合先粗定位。

bash
jcmd <pid> GC.class_histogram > /tmp/histo.txt

或者:

bash
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

导出前必须确认:

  1. 磁盘空间足够,dump 文件可能接近堆大小。
  2. 当前实例能否摘流量。
  3. 是否有副本可以承接流量。
  4. 是否已经保留应用日志、GC 日志、线程栈。

命令:

bash
jcmd <pid> GC.heap_dump /data/dump/app-oom.hprof

或:

bash
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是否线程栈持有大对象

结论必须落到引用链。例如:

text
heap dump 中 OrderExportTask 持有 ArrayList,Retained Heap 1.6GB。
引用链为 Thread -> ExportService.exportAll -> ArrayList -> OrderDTO[]。
根因是导出接口一次性查询并暂存 300 万条订单,没有分页和流式写出。

6. 直接内存和本地内存

如果是 Direct Memory、Metaspace、容器 OOMKilled,只看 heap dump 不够。

需要启动时开启 NMT:

bash
-XX:NativeMemoryTracking=summary

查看:

bash
jcmd <pid> VM.native_memory summary

看什么:

区域含义
Java HeapJava 堆
Class类元数据,和 Metaspace 相关
Thread线程栈和线程本地结构
CodeJIT 编译代码缓存
GCGC 自身本地结构
Internal / Symbol / ArenaJVM 内部结构

容器里要核算总预算:

text
容器 limit > Xmx + MaxMetaspaceSize + MaxDirectMemorySize + 线程数 * Xss + CodeCache + GC/JVM 开销

第三步:Arthas 排查思路

Arthas 适合进程还活着时在线诊断。它的优势是交互式强,可以快速看 JVM、线程、对象、调用栈、运行时参数;缺点是不能替代离线 MAT,某些命令也有性能风险。

1. 启动和连接

bash
java -jar arthas-boot.jar

选择目标进程后进入 Arthas 控制台。

如果服务器不能外网下载,生产应提前准备 arthas 包,并按公司安全规范执行。不要在未授权环境随意 attach。

2. 先看基础信息

bash
dashboard
jvm
sysprop
sysenv
vmoption

看什么:

命令作用
dashboardCPU、内存、GC、线程概览
jvmJVM 内存、GC、线程、类加载信息
vmoption查看 JVM 参数
sysprop系统属性
sysenv环境变量

如果 dashboard 中 old 区持续很高,Full GC 频繁,基本进入堆 OOM 排查。

3. 用 thread 看线程问题

bash
thread
thread -n 10
thread --state BLOCKED
thread <threadId>

用途:

  • 找 CPU 高线程。
  • 找大量 BLOCKED 线程。
  • 找卡在数据库、HTTP、MQ、锁的线程。
  • 判断是否线程池堆积。

如果 OOM 是队列堆积引起,线程栈常能看到消费者卡在下游,例如 JDBC、HTTP client、Redis、MQ ack 等。

4. 用 memory 和 heapdump 看内存

Arthas 可用:

bash
memory
heapdump /data/dump/arthas-heap.hprof

memory 用于看堆、非堆、直接内存等概况。heapdump 会导出堆快照,风险和 jmap 类似,仍要确认磁盘和停顿影响。

如果只需要快速看对象分布,可以用:

bash
heapdump --live /data/dump/arthas-live.hprof

注意:--live 会触发 Full GC,生产谨慎使用。

5. 用 vmtool 查看对象实例

vmtool 可以直接从 JVM 中查询某个类的实例,适合已经怀疑某个类泄漏时验证。

示例:查看某个业务对象实例数量。

bash
vmtool --action getInstances --className com.example.order.OrderDTO --limit 10

查看对象字段:

bash
vmtool --action getInstances --className com.example.order.OrderDTO --express 'instances[0]'

风险:不要对实例数量巨大、对象复杂的类随意查询太多实例。limit 必须控制。

6. 用 ognl 看静态缓存

如果怀疑本地静态缓存泄漏,可以用 ognl 看静态字段。

bash
ognl '@com.example.cache.LocalCache@CACHE.size()'

如果是 Caffeine、Guava Cache、本地 Map,可以看 size、key 分布、最大容量配置。

注意:ognl 能执行代码,有风险。生产只做只读表达式,不要调用清理、删除、写入等有副作用的方法,除非有明确应急审批。

7. 用 watch/trace 定位增长入口

如果内存还在持续上涨,要找是谁不断往集合、缓存、队列里放数据。

示例:观察缓存 put。

bash
watch com.example.cache.LocalCache put '{params, returnObj}' -n 5 -x 2

观察导出接口返回:

bash
trace com.example.export.ExportService exportAll '#cost > 1000'

观察某方法入参大小:

bash
watch com.example.importer.ImportService importBatch '{params[0].size()}' -n 10

风险控制:

命令风险控制
watch高频方法会产生大量输出-n、条件表达式、限制展开层级
trace高频链路有性能开销只追低频或加耗时条件
tt记录调用现场,占内存生产谨慎使用,及时清理
ognl可执行表达式只读、限权、不要改状态

Java 原生命令和 Arthas 怎么选

场景优先工具
进程已经挂了日志、GC 日志、heap dump、容器事件
进程还活着,需要标准证据jcmdjstatjstackjmap
需要交互式快速观察Arthas dashboardjvmthreadmemory
已怀疑某个类或缓存Arthas vmtoolognl
需要离线分析引用链heap dump + MAT
容器 OOMKilledKubernetes 事件、previous logs、NMT、内存预算

结论:Arthas 不是替代 Java 原生命令,而是在线诊断增强。最终证据仍然要能落到 dump、日志、引用链、代码位置和监控数据上。

典型场景一:接口导出导致 Heap OOM

现象:

text
java.lang.OutOfMemoryError: Java heap space

错误代码:

java
public List<OrderDTO> exportAll() {
    List<OrderDTO> orders = orderMapper.selectAll();
    return orders;
}

Java 原生命令思路:

bash
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.hprof

MAT 里如果看到 ArrayListOrderDTObyte[]char[] 占用大,并且引用链来自导出线程,就能定位为全量导出峰值过高。

Arthas 思路:

bash
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。
  • 限制最大导出范围。
  • 大导出改异步任务。
  • 导出文件落对象存储,前端轮询下载。

典型场景二:本地缓存无上限

错误代码:

java
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 原生命令:

bash
jcmd <pid> GC.class_histogram > /tmp/histo.txt
jcmd <pid> GC.heap_dump /data/dump/cache-oom.hprof

MAT 看:

text
System Class -> LocalCache -> CACHE -> ConcurrentHashMap -> Node[] -> value

Arthas:

bash
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 泄漏

错误代码:

java
public class UserContextHolder {
    private static final ThreadLocal<UserContext> LOCAL = new ThreadLocal<>();

    public static void set(UserContext context) {
        LOCAL.set(context);
    }
}

如果请求结束不 remove,线程池线程长期存活,value 可能一直挂在 ThreadLocalMap 上。

标准写法:

java
try {
    UserContextHolder.set(context);
    chain.doFilter(request, response);
} finally {
    UserContextHolder.remove();
}

Java 原生命令看 heap dump 引用链:

text
Thread -> ThreadLocalMap -> Entry -> value -> UserContext

Arthas 可以先看线程池线程和业务线程:

bash
thread
thread --state WAITING

如果已知 Holder 类,可以用 ognl 检查当前线程上下文,但这种问题通常最终仍要靠 heap dump 的引用链确认。

典型场景四:Direct Memory OOM

现象:

text
java.lang.OutOfMemoryError: Direct buffer memory

常见于 Netty、NIO、直接缓冲区未释放。

Java 原生命令:

bash
jcmd <pid> VM.flags
jcmd <pid> VM.native_memory summary
jcmd <pid> Thread.print > /tmp/thread.txt

前提是启动时开启:

bash
-XX:NativeMemoryTracking=summary

Arthas:

bash
memory
jvm

如果是 Netty:

bash
-Dio.netty.leakDetection.level=advanced

修复:

  • 检查 ByteBuf retain/release。
  • 不要把 DirectBuffer 长期放集合。
  • 复用缓冲区。
  • 合理设置 MaxDirectMemorySize
  • 容器内预留直接内存预算。

典型场景五:容器 OOMKilled

现象:

text
Kubernetes Last State: Terminated
Reason: OOMKilled
Exit Code: 137

这不一定有 Java OOM 栈。

排查:

bash
kubectl describe pod <pod>
kubectl logs <pod> --previous

核算:

text
进程总内存 = 堆 + 元空间 + 直接内存 + 线程栈 + CodeCache + GC/JVM 本地结构 + JNI

错误配置示例:

text
容器 limit 1024m
-Xmx900m
MaxDirectMemorySize 默认或较大
线程数 300,-Xss1m

这类配置很容易超过 1GB 被杀。

修复:

  • 降低 -Xmx,给堆外留空间。
  • 限制直接内存和元空间。
  • 收敛线程数。
  • 提高容器 limit。
  • 开启监控:RSS、heap、non-heap、direct、线程数、GC。

最后怎么写排查结论

面试或事故复盘要按证据链说,不要只说“内存不够”。

模板:

text
现象:
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 spaceMetaspaceDirect buffer memoryunable 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 原生命令怎么查

先用 jpsjcmd 找 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。先用 dashboardjvmmemory 看堆、非堆、GC 和线程概况;用 thread -nthread --state BLOCKED 看 CPU 高和阻塞线程;必要时用 heapdump 导出堆;如果怀疑某个类或缓存,用 vmtool --action getInstances 控制数量查看实例,用 ognl 只读查看静态缓存大小;如果内存持续增长,用 watchtrace 观察是谁不断写入缓存、集合或触发大查询。Arthas 命令要限制次数和条件,避免对高频方法造成额外压力。

关联知识点