OOM(out of memory)即内存溢出,在我们使用Java程序的时候,有可能会遇到内存空间被Java程序占满的情况的,此时就会形成OOM。
- 出现OOM的原因
Java程序出现OOM的原因大体有三个:
1.1 JVM内存设置过小
通常服务器上会有各种程序在运行着,本身就占用了较大的内存,而如果JVM(JavaVirtualMachine,Java虚拟机)的内存设置过小,可能会导致Java程序运行时所需内存大于实际分配的内存,存在内存溢出的现象,Java程序运行一段时间就会停止工作。
1.2 GC回收速度小于程序消耗速度
当GC回收内存的速度小于程序运行消耗的内存速度时,通常会往list和map中填充大量的数据,当内存十分紧张时,JVM即使是拆东补西也来不及,因而出现OOM。
1.3 存在内存泄漏情况
当存在打开文件不释放、不再使用的对象未断开引用关系和使用静态变量持有大对象引用等情况时,可能会导致内存泄漏情况的产生,久而久之也就形成了OOM。
- 常见OOM场景及解决方法
2.1 Java heap space
当堆空间没有足够的内存去存放新创建的对象时,就会给出“java.lang.OutOfMemoryError:Javaheap space”的报错,出现该错误的原因主要有以下几个方面:
①请求创建的是一个超大的对象,通常为一个数据组;
②超出预期的访问量或数据量,通常是上游的系统请求流量飙升,常见于各类促销或秒杀活动,可结合业务流量指标来查看是否有尖状峰值;
③过度使用终结器,导致对象没有被立即回收;
④由于内存泄漏导致大量对象引用无法释放,JVM无法对其进行回收。
通常情况下,在出现该问题时我们可以通过-Xmx参数调高堆内存大小来解决,如果无法通过调整堆内存大小解决,可尝试如下几种方法:
①如果请求创建的是超大对象,需检查其合理性,比如是否一次性查询了数据库全部结果,而没有做结果数限制;
②如果是业务峰值压力,可以考虑添加机器资源或者做限流降级;
③如果是内存泄漏,需要找到持有的对象,修改代码设计,比如关闭没有释放的连接。
2.2 GC overhead limit exceeded
当Java程序花费了98%的时间去执行GC缺只恢复了不到2%的内存空间,就会给出“java.lang.OutOfMemoryError:GC overhead limit exceeded”的报错。可以理解为程序已经耗用了所有的剩余内存也无法执行GC,该问题产生的原因和解决方法同第一点Java heap space部分类似。
2.3 Permgen sapce和Metaspace
Permgen sapce表示持久代(Permanent Generation)区域内存已满,通常可能是因为加载的class文件数量过多或者体积太大而造成的,在JDK1.8中使用Metaspace代替了Permgen sapce。当该部分报错时,我们需要根据具体情况来采取不同的措施:
①程序启动出错时,修改-XX:MaxPermSize(Metaspace中为-XX:MaxMetaspaceSize)启动参数,调整空间大小;
②应用重新部署时报错,可能是有应用没有完成重启,导致加载的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。
2.4 Unable to create new native thread
每个Java线程都要占用一定的内存空间,当JVM向底层操作系统请求一个新的native线程时,如果没有足够的资源分配就会报此类错误。当出现该类报错时,可采取的解决方法包括:
①升级配置,为机器提供更多的内存空间;
②降低堆内存空间大小;
③修复应用程序的线程泄漏问题;
④限制线程池的大小;
⑤使用-Xss参数来减少线程栈的大小;
⑥调高底层操作系统层面的最大线程数。
2.5 Out of swap space
虚拟内存有物理内存和交换空间两部分组成,当虚拟内存使用完毕后就会报此类错误,解决方法有:
①升级地址空间为64位;
②使用Arthas检查是否为Inflater/Deflater解压缩问题,如果是则显示调用 end方法;
③Direct ByteBuffer问题可以通过启动参数-XX:MaxDirectMemorySize调低阈值;
④升级服务器配置/隔离部署,避免争用。
2.6 Kill process or sacrifice child
OOM Killer这一内核作业会对所有进程进行打分,然后将评分较低的进程“杀死”,不同于其他的OOM错误, Kill process or sacrifice child 错误不是由JVM层面触发的,而是由操作系统层面触发的。解决该类报错的方法包括:
①升级服务器配置或隔离部署;
②OMM killer调优。
2.7 Requested array size exceeds VM limit
JVM对数组的最大长度做出了限制,当出现该类错误时则表示程序请求创建的数组超过最大长度限制。JVM在为数组分配内存前,会检查要分配的数据结构在系统中是否可寻址,通常为Integer.MAX_VALUE-2。此类问题比较罕见,通常需要检查代码,确认业务是否需要创建如此大的数组,是否可以拆分为多个块,分批执行。
2.8 Direct buffer memory
Java允许应用程序通过Direct ByteBuffer直接访问堆外内存,许多高性能程序通过Direct ByteBuffer来结合内存映射文件(Memory Mapped File)实现高效IO。Direct ByteBuffer的默认大小为64M,一旦超出了这个默认的限制就会报Direct buffer memory一类错误。解决该类错误的方法有:
①Java只能通过ByteBuffer.allocateDirect方法使用Direct ByteBuffer,因此,可以通过Arthas等在线诊断工具拦截该方法进行排查;
②检查是否直接或间接使用了NIO,如netty和jetty等;
③通过启动参数-XX:MaxDirectMemorySize调整Direct ByteBuffer的上限值;
④检查JVM参数是否有-XX:+DisableExplicitGC选项,如果有就去掉,因为该参数会使 System.gc() 失效;
⑤检查堆外内存使用代码来确认是否存在内存泄漏,或者通过反射调用sun.misc.Cleaner的clean()方法来主动释放被Direct ByteBuffer持有的内存空间;
⑥当内存容量确实不足的时候,需要升级配置。