前言
撸java代码的同学,多多少少都会碰到内存溢出(OOM)的场景,但是造成OOM原因却不止一个。今天就来总结一下常见的OOM的原因以及解决方案。
1、堆内存不足(Java heap space)
原因:
- 代码中可能存在大对象分配,通常是一个大树组。
- 可能存在内存泄露,导致多次GC之后,还是无法找到一块足够大的内存容纳当前对象,常见于使用了File等资源没有回收。
- 超出预期的访问量、数据量,通常是上游系统请求流量飙升,常见于各类促销、秒杀活动,可以结合业务流量指标排查是否有尖状峰值。
解决方案:
- 针对大部分的情况,通常只需要通过-Xmx参数调高JVM堆内存即可。如果仍然没有解决,可以参考下面几种情况进行处理。
- 如果是超大对象,检查其合理性,比如是否一次查询了数据库全部结果,而没有做结果数限制。
- 如果是业务峰值压力,可以考虑添加机器资源,或者做限流降级操作。
- 如果是内存泄露,需要找到持有对象,修改代码设计,比如关闭没有释放的连接。
2、永久代空间/元空间(Permgen space/ metaspace)
原因:
- 永久代是HotSot虚拟机对方法区的具体实现,存放被虚拟机加载的类信息、常量池、静态变量,JIT编译后的代码等
- Perngen的使用量和加载到内存的class的数量/大小正相关
解决方案:
- 检查是否永久代空间(启动参数:-XX:MaxPermSize)或者元空间(启动参数:-XX:MaxMetaspaceSize)设置的过小。
- 检查代码中是否存在大量反射操作
- dump之后通过mat检查是否存在大量由于反射生成的代理类
- 应用部署时报错,很可能没有重启应用,导致加载了多份class信息,重启JVM即可解决
3、GC overhead limit exceeded
原因:
- 当java进程花费98%以上时间来做GC并且回收了不到2%的堆内存时会抛出此异常。
- 堆内存太小
解决方案:
- 检查项目中是否有大量的死循环或者使用大内存的代码,优化代码
- dump内存,检查是否存在内存泄露,如果没有,加大内存。
4、方法栈溢出(Unable to create new native thread)
原因:
- 线程数超过操作系统最大线程数ulimit限制
- 线程数超过kernal.pid_max
- native 内存不足
解决方案:
- 升级配置,为机器提供更多内存
- 降低Java heap space大小
- 修复应用程序的线程泄露问题
- 限制线程池大小
- 使用-Xss参数减少线程栈大小
- 调高OS层面的线程最大数:执行ulimit -a 查看最大线程数限制,使用ulimit -u xx 调整最大线程数限制
5、swap区溢出(Out of swap space)
该错误表示所有可用的虚拟内存已被耗尽。虚拟内存(Virtual Memory)由物理内存(Physical)和交换空间(Swap Space)两部分组成。当运行时程序请求的虚拟内存溢出时就会报 Out of swap space错误。
原因:
- 地址空间不足
- 物理内存已耗光
- 应用程序的本地内存泄露,例如不断申请本地内存,却不释放
- 执行jmap -histo:live 命令,强制执行Full GC;如果执行几次后内存明显下降,则基本确认为Direct ByteBuffer问题
解决方案:
- 升级地址空间为64bit
- 使用Arthas检查是否为Inflater/Deflater 解压问题,如果是,则显示调用end方法
- Direct ByteBuffer问题可以通过启动参数–XX:MaxDirectMemorySize降低阈值
- 加大swap分区大小或者加大机器内存大小
- 隔离部署,避免争抢
6、分配超大数组(Requested array size exceeds VM limit)
原因:
这种情况一般是由于不合理数组分配请求导致的,在为数组分配内存之前,JVM会执行一项检查。要分配的数组在该平台是否可以寻址,如果不能寻址就会抛出这个错误。
解决方案:
检查代码中是否有创建超大数组的地方
7、Direct buffer memory
Java允许应用程序通过Direct ByteBuffer直接访问堆外内存,许多高性能程序通过Direct ByteBuffer结合内存映射文件(Memory MappedFile)实现高速IO
原因:
Direct ByteBuffer的默认大小为64MB,一旦使用超出限制,就会抛出Direct buffer memory错误。
解决方案:
Java只能通过ByteBuffer.allocateDirect方法使用DirectByteBuffer,因此,可以通过Arthas等在线诊断工具拦截该方法进行排查。
- 检查是否直接或间接使用了NIO,如netty,jetty等
- 通过启动参数-XX:MaxDirectMemorySize调整Direct ByteBuffer上限值
- 检查JVM参数是否有-XX:+DisableExplicitGC选项,如果有去掉,因为该参数会使System.gc()失效
- 检查堆外内存的代码,确认是否存在内存泄露;或者通过反射调用sun.misc.Cleaner的clean() 方法来主动释放Direct ByteBuffer持有的内存空间。
- 内存容量确实不够的,升级配置
8、Kill process or sacrifice child
有一种内核作业(Kernel Job)名为 Out of Memory Killer,它会在可用内存极低的情况下“杀死”(kill)某些进程。OOM Killer 会对所有进程进行打分,然后将评分较低的进程“杀死”,具体的评分规则可以参考 Surviving the Linux OOM Killer。
不同于其他的 OOM 错误,Kill processorsacrifice child错误不是由 JVM 层面触发的,而是由操作系统层面触发的。
原因:
默认情况下,Linux 内核允许进程申请的内存总量大于系统可用内存,通过这种“错峰复用”的方式可以更有效的利用系统资源。
然而,这种方式也会无可避免地带来一定的“超卖”风险。例如某些进程持续占用系统内存,然后导致其他进程没有可用内存。此时,系统将自动激活 OOM Killer,寻找评分低的进程,并将其“杀死”,释放内存资源。
解决方案:
- 升级服务器配置/隔离部署,避免争用。
- OOM Killer 调优。
结语
好了,今天就为大家分享到这里了。如果本文对你有帮助的话,欢迎点赞&收藏&分享,这对我继续分享&创作优质文章非常重要。感谢🙏🏻