系统稳定性—OutOfMemoryError常见原因及解决方法
当JVM
内存严重不足时,就会抛出java.lang.OutOfMemoryError
错误。本文总结了常见的OOM
原因及其解决方法,如下图所示。如有遗漏或错误,欢迎补充指正。
一、Java heap space
Java heap space
(堆内存溢出),当堆内存(Heap Space
)没有足够空间存放新创建的对象时,就会抛出java.lang.OutOfMemoryError: Java heap space
错误(根据实际生产经验,可以对程序日志中的OutOfMemoryError
配置关键字告警,一经发现,立即处理)。
1.1 原因分析
Java heap space
错误产生的常见原因可以分为以下几类:
- 内存泄漏(
Memory Leak
):大量对象引用没有释放,JVM
无法对其自动回收,常见于使用了File
等资源没有回收。 - 大对象:请求创建一个超大对象,比如数组、集合或缓冲区等,而堆的可用空间无法满足。
- 超出预期的访问量/数据量:通常是上游系统请求流量飙升,常见于各类促销/秒杀活动,可以结合业务流量指标排查是否有尖状峰值。
- 过度使用终结器(
Finalizer
),该对象没有立即被GC
。
1.2 解决方案
针对大部分情况,通常只需要通过增加-Xms
(初始堆大小)和-Xmx
(最大堆大小)参数的大小来调高JVM
堆内存空间即可。如:set JAVA_OPTS= -Xms1024m -Xmx1024m
(-Xms
和-Xmx
设置一样,是因为防止内存抖动)。
如果仍然没有解决,可以参考以下情况做进一步处理:
- 优化对象创建和销毁:确保在创建对象后及时销毁不再使用的对象,避免内存泄漏问题。可以使用合适的数据结构和算法来降低对象的创建数量,减少对堆空间的占用。例如使用迭代器代替直接在内存中存储大量数据,或者使用分页加载等技术,避免一次性加载过多的数据到内存中。
- 使用分代垃圾回收器:
Java
的垃圾回收机制中有分代回收的概念,根据对象的生命周期将堆分为新生代和老年代。新生代中的对象生命周期较短,可以使用快速的垃圾回收算法进行回收。老年代中的对象生命周期较长,可以使用更慢但更全面的垃圾回收算法进行回收。通过合理配置不同代的大小和比例,可以提高内存利用率,减少Java Heap Space
错误的发生。 - 使用虚拟机提供的工具:
Java
虚拟机提供了一些工具来帮助开发人员分析和调优内存使用情况。例如,使用Java VisualVM
可以监控堆的使用情况、查找内存泄漏等问题。通过这些工具可以快速定位并解决导致Java Heap Space
错误的原因。 - 业务峰值压力:可以考虑添加机器资源,或者做限流降级。
二、GC overhead limit exceeded
当Java
进程花费98%
以上的时间执行GC
,但只恢复了不到2%
的内存,且该动作连续重复了5
次,就会抛出java.lang.OutOfMemoryError:GC overhead limit exceeded
错误。简单地说,就是应用程序已经基本耗尽了所有可用内存,GC
也无法回收。
此类问题的原因与解决方案跟Java heap space
非常类似,可以参考上文。
三、Permgen space
Permgen space
(方法区内存溢出),该错误表示永久代(Permanent Generation
)已用满,通常是因为加载的class
数目太多或体积太大。
3.1 原因分析
永久代存储对象主要包括以下几类:
- 加载/缓存到内存中的
class
定义,包括类的名称,字段,方法和字节码; - 常量池;
- 对象数组/类型数组所关联的
class
; JIT
编译器优化后的class
信息。
PermGen
的使用量与加载到内存的class
的数量/大小正相关。
3.2 解决方案
根据Permgen space
报错的时机,可以采用不同的解决方案,如下所示:
- 程序启动报错,修改
-XX:MaxPermSize
启动参数,调大永久代空间。 - 应用重新部署时报错,很可能是没有应用没有重启,导致加载了多份
class
信息,只需重启JVM
即可解决。 - 运行时报错,应用程序可能会动态创建大量
class
,而这些class
的生命周期很短暂,但是JVM
默认不会卸载class
,可以设置-XX:+CMSClassUnloadingEnabled
和-XX:+UseConcMarkSweepGC
这两个参数允许JVM
卸载class
。
如果上述方法无法解决,可以通过jmap
命令dump
内存对象jmap -dump:format=b,file=dump.hprof <process-id>
,然后利用Eclipse MAT功能逐一分析开销最大的classloader
和重复class
。
四、Metaspace
JDK 1.8
使用Metaspace
替换了永久代(Permanent Generation
),该错误表示Metaspace
已被用满,通常是因为加载的class
数目太多或体积太大。
此类问题的原因与解决方法跟Permgen space
非常类似,可以参考上文。需要特别注意的是调整Metaspace
空间大小的启动参数为-XX:MaxMetaspaceSize
。
五、Unable to create new native thread
每个Java
线程都需要占用一定的内存空间,当JVM
向底层操作系统请求创建一个新的native
线程时,如果没有足够的资源分配就会报此类错误。
5.1 原因分析
JVM
向OS
请求创建native
线程失败,就会抛出Unable to create new native thread
,常见的原因包括以下几类:
- 线程数超过操作系统最大线程数
ulimit
限制。 - 线程数超过
kernel.pid_max
(只能重启)。 native
内存不足。
该问题发生的常见过程主要包括以下几步:
JVM
内部的应用程序请求创建一个新的Java
线程;JVM native
方法代理了该次请求,并向操作系统请求创建一个native
线程;- 操作系统尝试创建一个新的
native
线程,并为其分配内存; - 如果操作系统的虚拟内存已耗尽,或是受到
32
位进程的地址空间限制,操作系统就会拒绝本次native
内存分配; JVM
将抛出java.lang.OutOfMemoryError: Unable to create new native thread
错误。
5.2 解决方案
- 升级配置,为机器提供更多的内存;
- 降低
Java Heap Space
大小; - 修复应用程序的线程泄漏问题;
- 限制线程池大小;
- 使用
-Xss
参数减少线程栈的大小; - 调高
OS
层面的线程最大数:执行ulimia -a
查看最大线程数限制,使用ulimit -u xxx
调整最大线程数限制。
ulimit -a
.... 省略部分内容 .....
max user processes (-u) 16384
六、Out of swap space
该错误表示所有可用的虚拟内存已被耗尽。
虚拟内存(Virtual Memory
)由物理内存(Physical Memory
)和交换空间(Swap Space
)两部分组成。当运行时程序请求的虚拟内存溢出时就会报Out of swap space
错误。
6.1 原因分析
该错误出现的常见原因包括以下几类:
- 地址空间不足;
- 物理内存已耗光;
- 应用程序的本地内存泄漏(
native leak
),例如不断申请本地内存,却不释放。 - 执行
jmap -histo:live <pid>
命令,强制执行Full GC
;如果几次执行后内存明显下降,则基本确认为Direct ByteBuffer
问题。
6.2 解决方案
根据错误原因可以采取如下解决方案:
- 升级地址空间为
64
bit; - 使用Arthas检查是否为
Inflater/Deflater
解压缩问题,如果是,则显式调用end
方法。 Direct ByteBuffer
问题可以通过启动参数-XX:MaxDirectMemorySize
调低阈值。- 升级服务器配置/隔离部署,避免争用。
七、Kill process or sacrifice child
有一种内核作业(Kernel Job
)名为Out of Memory Killer
,它会在可用内存极低的情况下“杀死”(kill
)某些进程。OOM Killer
会对所有进程进行打分,然后将评分较低的进程“杀死”,具体的评分规则可以参考Surviving the Linux OOM Killer。
不同于其他的OOM
错误,Kill process or sacrifice child
错误不是由JVM
层面触发的,而是由操作系统层面触发的。
7.1 原因分析
默认情况下,Linux
内核允许进程申请的内存总量大于系统可用内存,通过这种“错峰复用”的方式可以更有效的利用系统资源。
然而,这种方式也会无可避免地带来一定的“超卖”风险。例如某些进程持续占用系统内存,然后导致其他进程没有可用内存。此时,系统将自动激活OOM Killer
,寻找评分低的进程,并将其“杀死”,释放内存资源。
7.2 解决方案
- 升级服务器配置/隔离部署,避免争用。
OOM Killer
调优。
八、Requested array size exceeds VM limit
JVM
限制了数组的最大长度,该错误表示程序请求创建的数组超过最大长度限制。
JVM
在为数组分配内存前,会检查要分配的数据结构在系统中是否可寻址,通常为Integer.MAX_VALUE - 2
。
此类问题比较罕见,通常需要检查代码,确认业务是否需要创建如此大的数组,是否可以拆分为多个块,分批执行。
九、Direct buffer memory
Direct buffer memory
(堆外内存),Java
允许应用程序通过Direct ByteBuffer
直接访问堆外内存,许多高性能程序通过Direct ByteBuffer
结合内存映射文件(Memory Mapped File
)实现高速IO
。
9.1 原因分析
Direct ByteBuffer
的默认大小为64MB
,一旦使用超出限制,就会抛出Direct buffer memory
错误。
9.2 解决方案
Java
只能通过ByteBuffer.allocateDirect
方法使用Direct ByteBuffer
,因此,可以通过Arthas等在线诊断工具拦截该方法进行排查。- 检查是否直接或间接使用了
NIO
,如netty
,jetty
等。 - 通过启动参数
-XX:MaxDirectMemorySize
调整Direct ByteBuffer
的上限值。 - 检查
JVM
参数是否有-XX:+DisableExplicitGC
选项,如果有就去掉,因为该参数会使System.gc()
失效。 - 检查堆外内存使用代码,确认是否存在内存泄漏;或者通过反射调用
sun.misc.Cleaner
的clean()
方法来主动释放被Direct ByteBuffer
持有的内存空间。 - 内存容量确实不足,升级配置。