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

JVM虚拟机异常排查,如何解决常见的OutOfMemoryError问题?java堆内存溢出怎么排查

导读JVM虚拟机异常排查的核心思路是:先确定OutOfMemoryError的具体类型,再结合堆转储分析、GC日志和监控工具定位内存泄漏点,最后通过代码修复或参数调优解决问题,本文围绕最常见的堆内存溢出、元空间溢出、栈溢出和直接内存溢出四种场景,给出完整的排查命令和调优方案,先分清OutOfMemoryError的……

JVM虚拟机异常排查的核心思路是:先确定OutOfMemoryError的具体类型,再结合堆转储分析、GC日志和监控工具定位内存泄漏点,最后通过代码修复或参数调优解决问题。本文围绕最常见的堆内存溢出、元空间溢出、栈溢出和直接内存溢出四种场景,给出完整的排查命令和调优方案。

先分清OutOfMemoryError的四种类型

很多开发者在遇到OOM时第一反应是加大-Xmx参数,但这样做往往治标不治本,JVM规范定义了多种OutOfMemoryError类型,每种类型的处理方式完全不同,如果连异常类型都没看清就盲目调参,要么问题复现,要么掩盖了更严重的代码缺陷。

以下是四种最常见的类型及触发场景:

  • java.lang.OutOfMemoryError: Java heap space:堆内存耗尽,常见于对象创建过多、集合类持有大量数据、代码中存在内存泄漏。
  • java.lang.OutOfMemoryError: Metaspace:元空间耗尽,常见于动态生成类、大量使用CGLib/ASM字节码增强、JSP热部署场景。
  • java.lang.OutOfMemoryError: unable to create new native thread:操作系统线程数超限,常见于无限制创建线程、操作系统线程数配置过低。
  • java.lang.OutOfMemoryError: Direct buffer memory:堆外直接内存耗尽,常见于NIO应用中ByteBuffer分配过多且未释放。

Java堆内存溢出的原因及解决办法

堆内存溢出是最常见的情况,也是大家问得最多的“jvm虚拟机异常排查”场景,先看现象:应用运行一段时间后,接口响应越来越慢,紧接着抛出Java heap space异常,服务失去响应。

常见的堆内存泄漏场景包括:

  • 数据库连接池或HTTP连接池关闭不彻底,每次请求都残留连接对象。
  • 使用了ThreadLocal但在线程复用场景下未清理,导致对象被线程对象长期持有。
  • 静态集合类不断添加数据,比如把某些缓存放在static Map中且没有过期策略。
  • 第三方SDK内部缓存无上限,例如某些日志组件的异步队列。

排查堆内存溢出时,建议按以下流程操作:

  • 第一步,确认堆内存使用趋势,使用jstat -gcutil <pid> 5000每5秒输出一次GC情况,观察Old区是否持续增长且GC后不下降,也可以使用jmap -heap <pid>查看当前堆使用量。

  • 第二步,生成堆转储文件,在启动参数中加入-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/logs/,让JVM在OOM时自动生成dump文件,如果服务还没挂,可以手动执行jmap -dump:format=b,file=heap.hprof <pid>。

  • 第三步,分析dump文件,使用MAT(Memory Analyzer Tool)打开堆转储文件,重点查看"Dominator Tree"和"Leak Suspects"报告,通常能直接看到哪个对象实例数量异常、被谁引用。

  • 第四步,定位泄漏代码,根据MAT分析结果,找到持有对象的GC Root引用链,对应到业务代码中的具体位置,修复资源未释放或逻辑错误。

堆内存调优参数的合理设置方式如下:

参数 作用 建议值
-Xms 初始堆大小 与-Xmx设相同值,避免运行期扩容
-Xmx 最大堆大小 根据服务器物理内存的50%-70%设置
-Xmn 新生代大小 堆大小的1/3到1/4
-XX:MaxMetaspaceSize 元空间上限 根据类加载量设置,一般512m-1g

大多数情况下,堆内存溢出不是因为堆太小,而是代码有缺陷,单纯调大-Xmx只会延缓OOM的出现时间,不会消除隐患,正确的做法是先定位泄漏点,再决定是否调整参数。

元空间溢出的典型场景与处理

元空间(Metaspace)用于存放类的元数据信息,JDK 8之后永久代(PermGen)被移除,元空间改为使用本地内存,默认情况下无上限,因此Metaspace OOM往往意味着类加载器泄漏或动态类生成失控。

易触发元空间溢出的常见场景:

  • Spring Boot应用使用spring-boot-devtools热部署功能,每次代码变更都会创建新的类加载器。
  • 大量使用动态代理(JDK Proxy、CGLib)且代理类没有被回收。
  • MyBatis等框架的Mapper XML文件数量巨大或反复加载。
  • Groovy脚本、JavaScript引擎等在运行时频繁编译执行代码。

排查元空间溢出的基本思路是限制元空间大小并观察类加载情况:

在启动参数中加入-XX:MaxMetaspaceSize=256m,让问题快速暴露,使用jstat -gcutil或jmap -clstats <pid>查看类加载器数量和Metaspace使用量,如果类加载器数量持续增加且回收不掉,基本可以断定是类加载器泄漏。

解决方案通常围绕以下三方面:

  • 关闭不必要热部署功能,生产环境不使用devtools。
  • 检查自定义类加载器是否正确卸载,loadClass和defineClass的使用是否规范。
  • 对Metaspace设置合理上限,避免无限增长拖垮整个操作系统。

栈溢出和线程数超限的排查思路

java.lang.StackOverflowError出现在递归调用没有终止条件、或者方法调用层级过深的场景,这种异常不是OutOfMemoryError,但经常会和OOM混淆在一起排查,解决方式较为简单:检查递归边界条件,将深层递归改为循环或迭代。

unable to create new native thread则表示JVM向操作系统申请创建新线程时失败,触发原因比较复杂:

  • 应用代码无限制创建线程,比如线程池未设置最大线程数。
  • 操作系统层面限制了进程可创建的线程数(ulimit -u)。
  • 服务器物理内存不足以支持更多线程的栈空间分配。

查看某台Linux服务器上进程的线程数,可以使用命令:ls /proc/<pid>/task | wc -l,如果是业务代码问题,需要排查线程池配置和并发逻辑;如果是操作系统限制,需要调整ulimit -u的值,或者在JVM参数中减少-Xss线程栈大小。

关于直接内存溢出的那些事

使用Netty框架或原生NIO编程时,ByteBuffer会分配堆外内存,这类内存不受-Xmx控制,默认上限等于-XX:MaxDirectMemorySize的值,而该参数不设置时默认等于-Xmx的大小。

直接内存溢出的典型症状是: 堆内存看起来一切正常,GC也很健康,但进程内存占用持续攀升,最终抛出Direct buffer memory异常,排查时重点检查代码中是否有ByteBuffer.allocateDirect()的调用而缺少对应的cleaner机制或DirectByteBuffer.release()方法。

给使用Netty的团队一个建议: 如果遇到线上JVM OOM排查而堆转储内没有明显异常,请先检查Netty的PooledByteBufAllocator和UnpooledByteBufAllocator的使用是否匹配,以及自定义Decoder中缓冲区是否及时释放。

线上JVM OOM排查的具体操作步骤

说到这里,需要梳理一套完整的线上JVM OOM排查流程,方便大家在实际生产环境中按步骤操作,这套流程适用于绝大多数Java服务:

执行以下命令收集现场数据:

  1. top查看进程CPU和内存占用,确认是否是JVM进程本身耗尽内存。
  2. jps -l找到目标Java进程的PID。
  3. jmap -heap <pid>查看堆内存配置和使用量。
  4. jmap -dump:format=b,file=heap.hprof <pid>生成堆转储快照。
  5. jstack <pid> > thread.log抓取线程快照,观察是否有线程死锁或大量BLOCKED状态。
  6. jstat -gcutil <pid> 1000持续观察GC频率和回收效果。

拿到这些数据后的分析顺序:

  • 先用MAT或jhat分析堆转储,判断是否存在明显的对象泄漏。
  • 再看GC日志,判断是内存泄漏还是内存分配速率过快导致的OOM。
  • 结合线程快照确认是否有请求并发量突刺、慢SQL积累等问题。

这套流程基本能覆盖绝大多数“jvm虚拟机异常排查”场景,也是阿里的Java开发手册中推荐的常规思路,业内专家指出,把现场数据第一时间保留下来,是成功排查OOM的第一原则。

如何避免内存溢出的常态化机制

与其每次出事都救火,不如建立预防机制,建议从以下层面入手:

  • 监控层面: 对JVM堆内存、GC耗时、Full GC次数等指标设置告警阈值,重点监控Old区使用率,超过70%就触发提醒。
  • 测试层面: 上线前做压力测试,模拟高峰流量下的内存使用情况,观察是否出现内存占用只升不降的情况。
  • 代码层面: 制定编码规范,严禁静态集合无限增长、确保所有连接流的关闭逻辑写在finally块中、正确处理ThreadLocal的remove方法。

关于JVM调优工具有个小发现: 很多人面对“Java内存溢出用什么工具好”时会反复纠结选MAT还是VisualVM还是Arthas,实际上不必过度纠结工具,选一个顺手的就行,MAT强大但上手难度稍高;VisualVM适合初学者,可视化界面友好;Arthas能在不重启服务的情况下在线排查,推荐用Arthas的dashboard命令快速查看内存和GC情况,再配合MAT做深度分析。

面对OOM时的常见误区

排查过程中,有几个误区容易让人走弯路:

  • 先调大堆内存再观察。 如上文所述,这可能掩盖真正的代码问题。
  • 忽视操作系统层面的限制。 有些OOM(比如native thread耗尽、直接内存溢出)和JVM参数关系不大,需要结合操作系统资源综合考虑。
  • 测试环境复现不了就认为问题不存在。 生产环境的数据量和并发模式很难在测试环境完全模拟,需要从代码中先找出所有可能的内存持有点,逐一排除。

最终建议

OOM的根因虽然各有不同,但排查路径是高度一致的:看清异常类型,保留现场数据,用工具分析,最后落到代码层面修复,团队是否具备一套成熟的JVM异常排查流程,直接决定了故障的恢复时长。建议将上述排查步骤固化到团队的应急预案中,让每位开发同学都掌握基本分析手段。

JVM OutOfMemoryError是什么原因:高频问题解答

问:发生了OOM之后有没有必要重启服务?

如果服务已经无法响应且堆转储已保留,直接重启恢复业务没有问题,但重启之前务必把dump文件和GC日志留存下来,否则问题复现时没有排查依据,如果服务还能维持,建议先抓取现场数据再决定是否重启。

问:为什么加大了堆内存OOM仍然频繁出现?

堆内存增大后,GC时长会变长,Full GC触发的阈值也被抬高,问题暴露的周期被拉长,但根本的代码缺陷不会因为内存变大而消失,多数情况下,加大内存只是给了你更多观察时间,而不是解决方案。

问:同一台服务器上部署多个Java应用时如何避免互相影响?

先给每个应用设置明确的堆内存上限并确保总和不超过物理内存的60%-70%,操作系统本身需要预留内存,使用容器部署时,要对JVM可见的CPU和内存做限制,避免某个应用的无限制增长拖垮整台服务器。

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