【业务功能篇132】生产环境中出现oom问题,排查思路

本文介绍了如何通过dmesg、ps、top、jstat和jmap等命令检查和诊断Java应用中的内存溢出问题,包括系统日志分析、进程监控、内存使用率追踪以及堆内存快照分析。作者强调了代码优化、jvm配置调整和解决内存泄露的重要性。

1、使用dmesg命令查看系统日志

dmesg |grep -E ‘kill|oom|out of memory’,可以查看操作系统启动后的系统日志,这里就是查看跟内存溢出相关联的系统日志。

2、这时候,需要启动项目,使用ps命令查看进程

ps -aux|grep java命令查看一下你的java进程,就可以找到你的java进程的进程id。

3、接着使用top命令

top命令显示的结果列表中,会看到%MEM这一列,这里可以看到你的进程可能对内存的使用率特别高。以查看正在运行的进程和系统负载信息,包括cpu负载、内存使用、各个进程所占系统资源等。

4、使用jstat命令

用jstat -gcutil 20886 1000 10命令,就是用jstat工具,对指定java进程(20886就是进程id,通过ps -aux | grep java命令就能找到),按照指定间隔,看一下统计信息,这里会每隔一段时间显示一下,包括新生代的两个S0、s1区、Eden区,以及老年代的内存使用率,还有young gc以及full gc的次数。

使用 jstat -gcutil 8968 500 5 表示每500毫秒打印一次Java堆状况(各个区的容量、使用容量、gc时间等信息),打印5次

例如:

看到的东西类似下面那样:

S0       S1     E            O         YGC  FGC

26.80   0.00   10.50    89.90    86     954

其实如果大家了解原理,应该知道,一般来说大量的对象涌入内存,结果始终不能回收,会出现的情况就是,快速撑满年轻代,然后young gc几次,根本回收不了什么对象,导致survivor区根本放不下,然后大量对象涌入老年代。老年代很快也满了,然后就频繁full gc,但是也回收不掉。

然后对象持续增加不就oom了,内存放不下了,爆了呗。

所以jstat先看一下基本情况,马上就能看出来,其实就是大量对象没法回收,一直在内存里占据着,然后就差不多内存快爆了。

5、使用jmap命令查看

执行jmap -histo pid可以打印出当前堆中所有每个类的实例数量和内存占用,如下,class name是每个类的类名([B是byte类型,[C是char类型,[I是int类型),bytes是这个类的所有示例占用内存大小,instances是这个类的实例数量。

6、把当前堆内存的快照转储到dumpfile_jmap.hprof文件中,然后可以对内存快照进行分析

使用jmap -dump:format=b,file=文件名 [pid],就可以把指定java进程的堆内存快照搞到一个指定的文件里去,但是jmap -dump:format其实一般会比较慢一些,也可以用gcore工具来导出内存快照

例如:jmap -dump:format=b,file=D:/log/jvm/dumpfile_jmap.hprof 20886

接着就是可以用MAT工具,或者是Eclipse MAT的内存分析插件,来对hprof文件进行分析,看看到底是哪个王八蛋对象太多了,导致内存溢出了

或者使用jdk的目录下的bin目录下的:jvisualvm.exe

8、总结:

一般常见的OOM,要么是短时间内涌入大量的对象,导致你的系统根本支持不住,此时你可以考虑优化代码,或者是加机器;要么是长时间来看,你的很多对象不用了但是还被引用,就是内存泄露了,你也是优化代码就好了;这就会导致大量的对象不断进入老年代,然后频繁full gc之后始终没法回收,就撑爆了

要么是加载的类过多,导致class在永久代理保存的过多,始终无法释放,就会撑爆

我这里可以给大家最后提一点,人家肯定会问你有没有处理过线上的问题,你就说有,最简单的,你说有个小伙子用了本地缓存,就放map里,结果没控制map大小,可以无限扩容,最终导致内存爆了,后来解决方案就是用了一个ehcache框架,自动LRU清理掉旧数据,控制内存占用就好了。

另外,务必提到,线上jvm必须配置-XX:+HeapDumpOnOutOfMemoryError,-XX:HeapDumpPath=/path/heap/dump。因为这样就是说OOM的时候自动导出一份内存快照,你就可以分析发生OOM时的内存快照了,到底是哪里出现的问题。

9、修改代码调优,修改jvm配置调优,部署接口压测

代码进行优化、根据压测的情况去进行一定的jvm参数的调优,一个系统的QPS,一个是系统的接口的性能,压测到一定程度的时候 ,机器的cpu、内存、io、磁盘的一些负载情况,jvm的表现

10、流程

附加:系统频繁full gc

比OOM稍微好点的是频繁full gc,如果OOM就是系统自动就挂了,很惨,你绝对是超级大case,但是频繁full gc会好多,其实就是表现为经常请求系统的时候,很卡,一个请求卡半天没响应,就是会觉得系统性能很差。

首先,你必须先加上一些jvm的参数,让线上系统定期打出来gc的日志:

-XX:+PrintGCTimeStamps

-XX:+PrintGCDeatils

-Xloggc:<filename>

这样如果发现线上系统经常卡顿,可以立即去查看gc日志,大概长成这样:

如果要是发现每次Full GC过后,ParOldGen就是老年代老是下不去,那就是大量的内存一直占据着老年代,啥事儿不干,回收不掉,所以频繁的full gc,每次full gc肯定会导致一定的stop the world卡顿,这是不可能完全避免的

接着采用跟之前一样的方法,就是dump出来一份内存快照,然后用Eclipse MAT插件分析一下好了,看看哪个对象量太大了

接着其实就是跟具体的业务场景相关了,要看具体是怎么回事,常见的其实要么是内存泄露,要么就是类加载过多导致永久代快满了,此时一般就是针对代码逻辑来优化一下。

给大家还是举个例子吧,我们线上系统的一个真实例子,大家可以用这个例子在面试里来说,比如说当时我们有个系统,在后台运行,每次都会一下子从mysql里加载几十万行数据进来各种处理,类似于定时批量处理,这个时候,如果对几十万数据的处理比较慢,就会导致比如几分钟里面,大量数据囤积在老年代,然后没法回收,就会频繁full gc。

当时我们其实就是根据这个发现了当时两台机器已经不够了,因为我们当时线上用了两台4核8G的虚拟机在跑,明显不够了,就要加机器了,所以增加了机器,每台机器处理更少的数据量,那不就ok了,马上就缓解了频繁full gc的问题了。

 https://www.cnblogs.com/c-xiaohai/p/12489336.html

<think>我们正在讨论OOM(内存溢出)的常见场景。根据引用内容,OOM可能发生在JVM的不同内存区域,包括堆、方法区(元空间)、栈内存和直接内存等。每个区域发生OOM的原因和表现各不相同。下面我将详细分析这些场景,并给出具体例子和解决方案的线索。 参考引用: [^1]: 提到方法区内存溢出的一种情况:`java.lang.OutOfMemoryError: GC overhead limit exceeded`,但注意这个错误实际上更常见于堆内存溢出(GC花费大量时间却回收了很少的内存)。而方法区溢出的典型错误是`java.lang.OutOfMemoryError: Metaspace`(Java8之后)或`java.lang.OutOfMemoryError: PermGen space`(Java8之前)。 [^2]: 指出OOM可能发生在不同内存区域,需要结合报错信息、JVM参数、代码逻辑等分析。 [^3]: 详细列出了OOM的七大经典场景,并给出了各区域的默认容量和调整参数。 下面我将按照内存区域来组织OOM的常见场景: ### 一、堆内存溢出(Java Heap Space) **错误信息**:`java.lang.OutOfMemoryError: Java heap space` **原因**: 1. **内存泄漏**:对象被无意中持有(如静态集合类持续添加元素、未关闭的资源等),导致无法被GC回收。 2. **内存设置过小**:`-Xmx`设置的最大堆内存不足,无法满足应用需求。 3. **数据量过大**:如一次性加载大量数据到内存(如大文件读取、大数据集处理)。 **示例代码**: ```java // 内存泄漏示例:静态Map持续添加数据且未移除 public class MemoryLeak { static Map<Object, Object> staticMap = new HashMap<>(); public static void main(String[] args) { while (true) { staticMap.put(new Object(), new byte[1024 * 1024]); // 不断添加大对象 } } } ``` **解决方案**: - 检查代码,修复内存泄漏(如使用弱引用、及时移除无用对象)。 - 增加堆内存:调整JVM参数`-Xmx`和`-Xms`。 - 优化程序,分批处理数据。 --- ### 二、方法区/元空间溢出(Metaspace) **错误信息**: - Java 8之前:`java.lang.OutOfMemoryError: PermGen space` - Java 8+:`java.lang.OutOfMemoryError: Metaspace` **原因**: 1. **类加载过多**:动态生成大量类(如使用CGLib、ASM等字节码技术),或重复加载类(如OSGi环境)。 2. **元空间设置过小**:默认无上限(受物理内存限制),但可通过`-XX:MaxMetaspaceSize`设置上限。 3. **常量池过大**:如大量字符串常量。 **示例场景**: ```java // 使用CGLib动态生成大量类 public class MetaspaceOOM { public static void main(String[] args) { while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); // 禁用缓存,每次创建新类 enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1)); enhancer.create(); // 持续生成新类 } } static class OOMObject {} } ``` **解决方案**: - 增加元空间大小:`-XX:MaxMetaspaceSize=256m`。 - 减少动态类生成(如启用缓存)。 - 检查类加载器泄漏(如Web应用重启后旧类加载器未卸载)。 --- ### 三、栈内存溢出(Stack Overflow) **错误信息**:`java.lang.StackOverflowError`(严格来说不属于OOM,但同属内存错误) **原因**: 1. **无限递归**:递归调用层数过深(如未设置终止条件)。 2. **栈帧过大**:单个方法调用占用栈空间过大(如大量局部变量)。 3. **线程数过多**:每个线程分配独立栈空间,线程过多导致总栈内存耗尽。 **示例代码**: ```java // 无限递归导致栈溢出 public class StackOverflowExample { public static void recursiveMethod() { recursiveMethod(); // 无限递归 } public static void main(String[] args) { recursiveMethod(); } } ``` **解决方案**: - 检查递归终止条件。 - 增加栈深度:调整`-Xss`参数(如`-Xss2m`)。 - 减少线程数或减小单个线程栈大小(需权衡)。 --- ### 四、直接内存溢出(Direct Buffer Memory) **错误信息**:`java.lang.OutOfMemoryError: Direct buffer memory` **原因**: 1. **NIO操作**:使用`ByteBuffer.allocateDirect()`申请堆外内存,未及时释放。 2. **直接内存设置过小**:默认与堆最大值`-Xmx`相同,可通过`-XX:MaxDirectMemorySize`调整。 3. **内存泄漏**:DirectByteBuffer对象被GC回收后,堆外内存由`Cleaner`释放,但若`Cleaner`未触发则泄漏。 **示例代码**: ```java // 持续申请直接内存不释放 public class DirectMemoryOOM { public static void main(String[] args) throws Exception { List<ByteBuffer> list = new ArrayList<>(); while (true) { ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024); // 每次1MB list.add(buffer); // 持有引用阻止GC } } } ``` **解决方案**: - 显式释放直接内存:调用`((DirectBuffer) buffer).cleaner().clean()`。 - 增加直接内存上限:`-XX:MaxDirectMemorySize=256m`。 - 避免频繁申请/释放(可复用缓冲区)。 --- ### 五、GC Overhead Limit Exceeded **错误信息**:`java.lang.OutOfMemoryError: GC overhead limit exceeded` **原因**: - JVM花费超过98%的时间进行GC,但仅回收不到2%的堆内存(通常由**堆内存泄漏**引起)。 - 本质是堆内存溢出的一种特殊表现。 **解决方案**: - 同堆内存溢出:检查内存泄漏,增加堆大小。 - 关闭该保护机制(不推荐):`-XX:-UseGCOverheadLimit`。 --- ### 六、其他场景 1. **创建线程溢出**:`java.lang.OutOfMemoryError: unable to create new native thread` - 原因:操作系统限制线程数(如`ulimit -u`)、进程虚拟内存耗尽。 - 解决:减少线程数、调整系统线程数限制。 2. **数组溢出**:`java.lang.OutOfMemoryError: Requested array size exceeds VM limit` - 原因:申请超过`Integer.MAX_VALUE`的数组(如`new byte[Integer.MAX_VALUE]`)。 --- ### 总结:OOM排查思路 1. **看错误信息**:确定溢出区域(堆、元空间、栈等)。 2. **查JVM参数**:检查各区域内存设置是否合理。 3. **分析内存快照**: - 堆溢出:用`jmap -dump`导出堆转储,MAT分析对象占用。 - 元空间溢出:用`jstat -gc`监控类加载数量。 4. **代码审查**:重点检查资源释放、大对象使用、递归等。 > ⚠️ **注意**:生产环境推荐开启JVM参数记录OOM时的堆转储: > `-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump` --- §§ 相关问题 §§ 1. 如何用MAT(Memory Analyzer Tool)分析堆转储文件? 2. JVM哪些参数可以预防OOM? 3. 如何在Docker容器中调试JVM内存问题? 4. 直接内存泄漏如何定位? 5. 如何监控JVM各内存区域的使用情况?</think>### JVM 内存溢出(OOM)常见场景详解 内存溢出(OOM)是 Java 程序常见的严重错误,发生在 JVM 内存区域无法满足分配需求时。以下是 **7 大经典 OOM 场景**及其原理分析: --- #### 一、堆内存溢出(Java Heap Space) **错误信息**:`java.lang.OutOfMemoryError: Java heap space` **根本原因**: - **对象数量超过堆容量**:长期存活对象堆积(内存泄漏)或短期创建海量对象 - **内存设置不足**:`-Xmx` 值过小或未根据业务负载调整 **典型场景**: 1. 内存泄漏:静态集合类持续添加元素(如 `static Map` 缓存未清理) 2. 大文件处理:一次性加载超大文件到内存(如 CSV 解析未分页) 3. 高并发创建对象:秒杀系统未限制请求量导致瞬时对象暴增 **解决方案**: - 检查内存泄漏(MAT 分析堆转储) - 增加堆大小:`-Xmx4g -Xms4g` - 优化代码:分页处理数据,使用弱引用缓存[^3] --- #### 二、元空间溢出(Metaspace) **错误信息**:`java.lang.OutOfMemoryError: Metaspace`(Java 8+) **根本原因**: - 动态生成大量类(如 CGLib 代理、OSGi 热部署) - JSP 频繁编译或反射滥用(`MethodHandles`) **典型场景**: 1. 框架动态代理:Spring AOP 未限制代理类缓存 2. 热加载服务:持续部署导致旧类未卸载 3. 反射工具滥用:大量 `Class.forName()` 调用 **解决方案**: - 限制元空间大小:`-XX:MaxMetaspaceSize=512m` - 关闭无用特性:`-noverify` 跳过字节码验证 - 监控类加载数:`jcmd <pid> GC.class_stats`[^3] --- #### 三、GC 开销超限(GC Overhead Limit Exceeded) **错误信息**:`java.lang.OutOfMemoryError: GC overhead limit exceeded` **根本原因**: - **GC 效率低下**:98% 时间用于 GC 但回收内存 < 2% - **对象分配速率过高**:短周期对象疯狂创建 **典型场景**: 1. 循环内创建大对象:`while(true) { new byte[1024*1024]; }` 2. 低效字符串处理:循环拼接 `String`(未用 `StringBuilder`) **解决方案**: - 优化代码逻辑减少对象创建 - 调整 GC 策略:改用 G1 或 ZGC - 关闭保护机制(不推荐):`-XX:-UseGCOverheadLimit`[^1] --- #### 四、栈内存溢出(Stack) **错误信息**:`java.lang.StackOverflowError` **根本原因**: - **线程栈深度超标**:递归调用未设终止条件 - **局部变量过多**:单个方法栈帧过大 **典型场景**: 1. 无限递归:未设置递归基线(base case) ```java void recursive() { recursive(); // 无限调用 } ``` 2. 大栈帧方法:方法内声明超大数组(如 `byte[1000000]`) **解决方案**: - 增加栈容量:`-Xss2m`(默认 1MB) - 改写递归为迭代(循环) - 拆分复杂方法[^3] --- #### 五、直接内存溢出(Direct Buffer Memory) **错误信息**:`java.lang.OutOfMemoryError: Direct buffer memory` **根本原因**: - NIO 堆外内存分配过量(`ByteBuffer.allocateDirect()`) - 未及时释放 `DirectByteBuffer` **典型场景**: 1. 文件通道操作:`FileChannel.transferTo()` 处理大文件 2. 网络编程:Netty 未配置 `-XX:MaxDirectMemorySize` **解决方案**: - 显式调用 `((DirectBuffer) buffer).cleaner().clean()` - 限制直接内存:`-XX:MaxDirectMemorySize=1g` - 使用池化技术(如 Netty 的 `PooledByteBufAllocator`)[^3] --- #### 六、线程数溢出(Unable to Create Thread) **错误信息**:`java.lang.OutOfMemoryError: unable to create new native thread` **根本原因**: - 操作系统线程数超限(`ulimit -u`) - 线程栈总内存超过进程空间 **典型场景**: 1. 线程池配置错误:`Executors.newCachedThreadPool()` 无上限 2. 递归创建线程:`while(true) new Thread().start()` **解决方案**: - 使用有界线程池:`new ThreadPoolExecutor(core, max)` - 调整系统限制:`ulimit -u 4096`(Linux) - 减少线程栈大小:`-Xss256k`[^2] --- #### 七、数组超限(Requested Array Size) **错误信息**:`java.lang.OutOfMemoryError: Requested array size exceeds VM limit` **根本原因**: - 申请超过 `Integer.MAX_VALUE` 的数组 - 堆内存碎片化导致连续空间不足 **典型场景**: ```java int[] arr = new int[Integer.MAX_VALUE]; // 需要 8GB 连续内存! ``` **解决方案**: - 分块处理数据(如分批加载文件) - 使用集合替代数组(如 `ArrayList` 自动扩容) - 申请压缩数组(`-XX:+UseCompressedOops`)[^3] --- ### OOM 排查工具箱 | 工具 | 用途 | 命令示例 | |---------------------|-----------------------------|----------------------------| | **jmap** | 堆转储分析 | `jmap -dump:format=b,file=heap.bin <pid>` | | **jstat** | GC 实时监控 | `jstat -gcutil <pid> 1000` | | **MAT** | 内存泄漏分析 | 分析 heap.bin 文件 | | **Arthas** | 在线诊断 | `watch demoService * '{params,returnObj}'` | | **Native Memory Tracking** | 追踪堆外内存 | `-XX:NativeMemoryTracking=detail` | > 💡 **黄金法则**: > 生产环境必加 JVM 参数: > ```bash > -XX:+HeapDumpOnOutOfMemoryError > -XX:HeapDumpPath=/logs/oom_dump.hprof > ``` ---
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值