服务器与大带宽专家 · 持牌IDC/CDN/ISP服务商
简米科技官网JIANMI TECH
资讯 2026-11-13 简米科技 3,462 字 8 分钟阅读

jvm虚拟机异常排查,如何解决常见的OutOfMemoryError问题?jvm oom怎么排查解决

导读OutOfMemoryError不是“内存不够”的句号,而是排查的起点:先确认错误子类型,再抓堆转储,最后用MAT定位持有链,多数线上OOM都能在半小时内锁定方向,下面按实操顺序拆开讲,涉及参数、命令和工具路径,可以直接照着做,JVM内存溢出排查步骤:从现象到根因的完整路径第一步:别急着调大内存,先看异常子类型……

OutOfMemoryError不是“内存不够”的句号,而是排查的起点:先确认错误子类型,再抓堆转储,最后用MAT定位持有链,多数线上OOM都能在半小时内锁定方向。下面按实操顺序拆开讲,涉及参数、命令和工具路径,可以直接照着做。

JVM内存溢出排查步骤:从现象到根因的完整路径

第一步:别急着调大内存,先看异常子类型

JVM抛出的OOM从来不是一句笼统的话,后面跟着后缀,后缀决定了排查方向:

  • Java heap space:堆内存不够,多半是对象泄漏或一次性加载数据过大
  • GC overhead limit exceeded:GC回收效率极低,回收时间占比超过限制,本质还是堆吃紧
  • Metaspace:类元数据区撑爆,常见于动态生成类、热部署、CGLIB代理滥用
  • Direct buffer memory:堆外内存耗尽,NIO、Netty场景高发
  • unable to create new native thread:线程数超限,和操作系统ulimit -u有关
  • Requested array size exceeds VM limit:一次性申请超大数组

看到日志的第一眼就把后缀抄下来,后面的动作完全不同,业内专家指出,超过一半的误判来自“看到OOM就加-Xmx”,结果把泄漏问题掩盖成更晚的爆发。

第二步:让JVM在崩溃瞬间自己留证据

线上服务不要等人工介入,先把参数配好:

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dump/heap.hprof
-XX:+PrintGCDetails -Xlog:gc:file=/data/logs/gc.log:time

JDK 8用-XX:+PrintGCDetails,JDK 11及以上换成-Xlog:gc,这两个开关成本很低,收益很大,没有它们,事发后只能靠复现。

如果事发时进程还活着,立即手动抓:

jcmd <pid> GC.heap_dump /data/dump/heap_manual.hprof
# 或
jmap -dump:format=b,file=heap.hprof <pid>

同时抓一份线程栈:jstack <pid> > thread.txt,堆和线程栈一起看,能快速区分“真泄漏”和“瞬时峰值”。

第三步:用工具读dump,别用眼睛猜

拿到hprof文件后,推荐Eclipse MAT:

  • 打开后先看Leak Suspects报告,它会直接给出可疑对象和占比
  • 用Dominator Tree找“谁持有谁”,从GC Roots一路往下
  • 用Histogram按类统计实例数,重点看业务自定义类,不是java.lang.String

常见的泄漏模式很固定:静态Map只增不减、ThreadLocal没remove、连接池对象没归还、缓存无上限,把这些类名在代码里搜一遍,往往比读整份dump更快。

四类常见OOM的针对性解法

Java heap space:先区分泄漏还是峰值

  • 如果是泄漏:MAT里对象数量随时间线性增长,修代码,比如给缓存加容量上限或改用弱引用
  • 如果是峰值:堆确实不够,调-Xms与-Xmx设为相同值,避免动态扩堆抖动,再配合压测验证

调参不是万能药,行业共识认为,堆大小应与存活对象集匹配,盲目翻倍只会把Full GC的停顿拉长。

Metaspace溢出常见原因与处理

Metaspace溢出在微服务和热部署环境里很常见,几个高频原因:

  1. 反射、CGLIB、ByteBuddy持续生成代理类,且类加载器没释放
  2. 应用热部署,旧的ClassLoader被业务代码持有,元数据无法回收
  3. 大量JSP或动态脚本编译,类数量暴涨

处理思路是先限制再定位:设置-XX:MaxMetaspaceSize=256m让问题尽早暴露,再用jcmd <pid> VM.metaspace或jmap -clstats <pid>看类加载器分布,找到那个一直增长的ClassLoader,顺着引用链改代码。

GC overhead limit exceeded

这个错误的潜台词是“我在疯狂GC但几乎收不回空间”,优先排查:

  • 堆设置过小,存活对象已接近上限
  • 存在大对象短生命周期反复创建,比如循环里拼字符串、频繁new大数组
  • 使用了不适合的垃圾收集器,小堆配G1反而增加开销

Direct buffer memory与线程创建失败

堆外内存问题看-XX:MaxDirectMemorySize,并检查Netty的ByteBuf是否release,线程创建失败则先看ulimit -u和线程池上限,很多事故是线程池没设队列上限,任务堆积又不断新建线程。

堆内存溢出和栈溢出有什么区别?一张表看懂

很多人把两者混为一谈,其实触发位置和现象完全不同。

维度 堆内存溢出 栈溢出
异常类型 OutOfMemoryError: Java heap space StackOverflowError
发生区域 堆 线程栈
典型原因 对象泄漏、缓存无界、大对象 递归过深、栈帧过大
是否进程退出 通常导致进程不可用 单线程抛出,进程可能存活
排查工具 MAT、jmap、GC日志 jstack、递归调用链审查
调参方向 -Xmx、对象生命周期治理 -Xss、改递归为迭代

看清这张表,就不会出现“栈溢出却去加堆内存”的误操作。

线上Java服务OOM怎么快速定位

线上排查讲究顺序,别一上来就上重型工具,一个可复用的路径:

  1. 看监控:堆使用率、Full GC次数、GC耗时是否在事发前陡增
  2. 看日志:确认OOM子类型和发生时间点
  3. 拿dump:自动dump优先,没有就手动补抓
  4. 本地分析:MAT打开,锁定Top消费者
  5. 回滚或止血:先降级大流量入口,再改代码

止血和根因分析要分开做,生产环境先恢复可用性,dump留着慢慢看,这两件事不冲突。

预防优于救火:参数配置与压测

上线前把这几件事做了,能挡掉大部分问题:给所有本地缓存设上限并配淘汰策略;对批量查询加分页;对ThreadLocal统一在finally里remove;压测时用和生产相同的堆参数跑满24小时,观察老年代增长曲线,曲线持续上扬就是泄漏信号,比任何告警都直观。

自建监控还是买商业APM:成本账怎么算

团队常纠结这一点,自建基于Prometheus加Micrometer,人力成本主要在搭建和告警调优;商业APM按主机或探针数计费,JVM调优服务的市场报价差异较大,从几千元一次的专项诊断到按年订阅的监控平台都有,判断标准很简单:如果OOM一年发生不到两次,抓dump加MAT足够;如果核心交易链路对停顿敏感,商业工具的火焰图和实时堆分析能省下大量排查时间。

收束

OOM排查的核心不是调参数,而是“先分类、再取证、后定位”这条固定链路,把HeapDumpOnOutOfMemoryError和GC日志当成标配,把MAT当成日常工具,多数问题都不会拖成事故。

Q&A:JVM虚拟机OOM排查常见问题

Q1:没有配置自动dump,事发后进程已经重启,还能查吗?
能查一部分,GC日志、监控里的堆曲线、应用日志中的错误堆栈都能还原趋势;如果开启了jcmd定时采样或APM的堆快照,也能拿到历史数据,完全没留证据时,只能靠复现,成本高很多,所以自动dump参数建议默认开启。

Q2:堆内存调到物理内存的80%合理吗?
不合理,JVM之外还有堆外内存、元空间、线程栈、代码缓存,以及操作系统自身的页缓存,容器环境下更要以容器限额为准,忽略UseContainerSupport会导致JVM按宿主机内存计算,直接触发OOMKilled。

Q3:OOM一定是代码问题吗?
不全是,堆大小配置与业务负载不匹配、容器内存限额低于-Xmx、JDK版本升级后默认收集器变化,都会引发OOM,排查时应先排除配置与运行环境因素,再进入代码层分析,这样能避免在错误的层面上耗费时间。

分享本文
本文为 简米科技官网 原创,已由运维技术专家审核。转载请注明来源:原文链接
售前咨询 服务热线 售后 邮箱