OOM常见各种场景及解决方案

1,504 阅读5分钟

“这是我参与更文挑战的第9天,活动详情查看: 更文挑战

上篇介绍了Java GC日志详解,接下来介绍OOM常见各种场景及解决方案。

堆溢出

报错信息

java.lang.OutOfMemoryError:Java heap space

JVM参数配置

参数配置:初始-Xms30M -Xmx30M

-XX:+PrintGCDetails -XX:MetaspaceSize=64m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap/heapdump.hprof -XX:+PrintGCDateStamps -Xms200M -Xmx200M -Xloggc:log/gc-oomHeap.log

原因及解决方案

原因

  1. 代码中可能存在大对象分配
  2. 可能存在内存泄漏,导致在多次GC之后,还是无法找到一块足够大的内存容纳当前对象。

解决方法

  1. 检查是否存在大对象的分配,最有可能的是大数组分配
  2. 通过jmap命令,把堆内存dump下来,使用MAT等工具分析以下,检查是否存在内存泄漏的问题
  3. 如果没有找到明显的内存泄漏,使用-Xmx加大堆内存
  4. 还有一点容易被忽略,检查是否有大量的自定义的Finalizable对象,也有可能是框架内存提供的,考虑其存在的必要性

dump文件分析

  1. jvisualvm分析
  2. MAT分析

gc日志文件

元空间溢出

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、即时编译器编译后的代码等数据。虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做Non-Heap(非堆),目的应该是与Java堆区分开来。

元空间存储数据类型

报错信息

java.lang.OutOfMemoryError:Metaspace

JVM参数配置

-XX:+PrintGCDetails -XX:MetaspaceSize=60m -XX:MaxMetaspaceSize=60m -Xss512k -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap/heapdumpMeta.hprof -XX:SurivorRatio=8 -XX:+TraceClassLoading -XX:+TraceClassUnloading -XX:+PrintGCDateStamps -Xms60M -Xmx60M -Xloggc:log/gc-oomMeta.log

原因及解决方案

JDK8后,元空间替换了永久代,元空间使用的是本地内存

原因

  1. 运行期生成了大量的代理类,导致方法区被撑爆,无法卸载
  2. 应用长时间运行,没有重启
  3. 元空间内存设置过小

解决方法

因为该OOM原因比较简单,解决方法有如下几种

  1. 检查是否永久代空间或者元空间设置的过小
  2. 检查代码中是否存在大量的反射操作
  3. dump之后通过mat检查是否存在大量由于反射生成的代理类

GC overhead limit exceeded

案例模拟

示例代码1

JVM配置

-XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap/dumpExceeded.hprof -XX:+PrintGCDateStamps -Xms10M -Xmx10M -Xloggc:log/gc-oomExceeded.log

报错信息

示例代码2

JVM配置

-XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heap/dumpExceeded1.hprof -XX:+PrintGCDateStamps -Xms10M -Xmx10M -Xloggc:log/gc-oomExceeded1.log 

代码解析

第一段代码:运行期间将内容放入常量池的典型案例

intern()方法

  1. 如果字符串常量池里面已经包含了等于字符串X的字符串,那么就返回常量池中这个字符串的引用
  2. 如果常量池中不存在,那么就会把当前字符串添加到常量池并返回这个字符串的引用

第二段代码:不停的追加字符串str

  1. Java heap space的demo每次都能回收大部分的对象(中间产生的UUID),只不过有一个对象是无法回收的,慢慢长大,直到内存溢出
  2. GC overhead limit exceeded的demo由于每个字符串都在被list引用,所以无法回收,很快就用完内存,触发不断回收的机制。

分析及解决

原因

这个是JDK6新加的错误类型,一般都是堆大小导致的。Sun官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。本质是一个预判性的异常,抛出该异常时系统没有真正的内存溢出。

解决方法

  1. 检查项目中是否有大量的死循环或有使用大内存的代码,优化代码。
  2. 添加参数-XX:-UseGCOverheadLimit禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,最终出现java.lang.OutOfMemoryError: java heap space。
  3. dump内存,检查是否存在内存泄漏,如果没有,加大内存。

线程溢出

报错信息

java.lang.OutOfMemoryError: unable to create new native Thread

问题原因

出现这种异常,基本上都是创建了大量的线程导致的

分析及解决

原因

是否必须创建大量的线程,能创建的线程数的具体计算公式如下: (MaxProcessMemory - JVMMemory -ReserverdOsMemory) / ThreadStackSize = Number of Threads

  1. MaxProcessMemory 指的是进程可寻址的最大空间
  2. JVMMemory JVM内存
  3. ReserverdOsMemory 保留的操作系统内存
  4. ThreadStackSize 线程栈的大小

解决方向

  1. 如果程序中有bug,导致创建大量不需要的线程或者线程没有及时回收,那么必须解决这个bug,修改参数是不能解决问题的。
  2. 如果程序确实需要大量的线程,现有的设置不能达到要求,那么可以通过修改MaxProcessMemory,JVMMemory,ThreadStackSize这三个因素,来增加能创建的线程数。

总结

本节介绍了Java中OOM常见的各种场景及解决方案,具体问题具体分析,不可能覆盖所有业务场景,也没有万能的定律和规律可循。下一篇给大家介绍性能优化方案。

欢迎大家关注公众号(MarkZoe)互相学习、互相交流。