oom!该懂的都懂

2,770 阅读4分钟

OOM,不管是菜鸟程序猿还是资深砖家,在开发过程中都经常会遇到的问题。

下面围绕几点,来“浅入浅出”了解下OOM。

what OOM (什么是OOM)?

why OOM(为什么会发生OOM)?

where OOM(哪里会发生OOM)?

when OOM(什么时候会发生OOM)?

what OOM ?

OOM,全称“Out Of Memory”,内存溢出,通俗理解就是内存不够啦。

why OOM ?

  • 服务的正常运行需要的内存过多,而JVM设置的内存过小,导致服务跑不起来或者运行一段时间后挂掉。
  • GC回收内存的速度赶不上程序运行内存消耗内存的速度。一般是大对象、大数组导致。
  • 内存泄漏问题,长期内存泄会导致内存溢出。比如打开文件不释放、创建链接不释放、大量不再使用的对象但未断开引用关系等。

内存泄漏:是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。

内存溢出:指程序申请内存时,没有足够的内存供申请者使用。

where OOM ?

JVM运行时数据区五个区域中,除了程序计数器不会发生OOM,其他所有区域都有可能。

除了虚拟机栈、本地方法栈、堆、方法区/元空间以外,还有直接内存也会发生OOM。

直接内存:直接内存虽然不是虚拟机运行时数据区的一部分,但既然是内存,就会受到物理内存的限制。

OOM一共有9个,加上栈溢出共10个,下面是几个常见的。

① java.lang.OutOfMemoryError:Javaheap space

发生在堆空间,没有足够空间存放新创建的对象。

造成原因

  • 可能创建了超大对象,通常是大数组。
  • 业务高峰期。
  • 内存泄漏,大量对象引用没释放,JVM无法对其回收。

解决方案

  • 使用 -Xmx 参数调整堆大小。
  • 针对业务峰值,可以考虑增加机器,或者限流降级。
  • 针对内存泄漏,需要找到相关对象,修改代码。

② java.lang.OutOfMemoryError:GC overhead limit exceeded

当 Java 进程花费 98% 以上的时间执行 GC,但只恢复了不到 2% 的内存,且该动作连续重复了 5 次,会抛出java.lang.OutOfMemoryError:GC overhead limit exceeded 错误。

③ java.lang.OutOfMemoryError:Permgen space 和 Metaspace

Permgen space 发生在永久代,但在JDK1.8已废弃,表示永久代(Permanent Generation)已用满,通常是因为加载的 class 数目太多或体积太大。

Metaspace 是 JDK 1.8 使用 Metaspace 替换了永久代(Permanent Generation),该错误表示 Metaspace 已被用满,通常是因为加载的 class 数目太多或体积太大。

④ java.lang.StackOverflowError

栈内存溢出,一般出现这个问题是因为程序里有死循环或递归调用所产生的。

when OOM ?

最简而易见的就是有报错,出现 java.lang.OutOfMemoryError ,这就是发生了OOM。还有就是频繁GC事件发生,当发现Full GC,Young GC发生频繁时,很有可能就是在OOM的边缘疯狂试探。

一个线程OOM后,其他线程还能运行吗?

答案:能。发生OOM后的线程一般情况下会死亡,也就是会被终结掉,该线程持有的对象占用的heap都会被gc了,释放内存。因为发生OOM之前要进行GC,就算其他线程能够正常工作,也会因为频繁gc产生较大的影响,比如像我们的服务在OOM前频繁GC的过程中,会出现请求超时的现象。

一次线上OutOfMemoryError记录

背景

在通过JDBC读取数据库数据的时候,由于在没有限制条数的情况下,将200w条数据写入List中,引发 java.lang.OutOfMemoryError:Javaheap space 异常。

模拟

        List<DataBean> datas = new ArrayList<>();
        while (true){
            datas.add(new DataBean("123"));
        }
  • 服务监控报警

首先可以通过服务监控告警,会有服务节点堆内存使用过载异常通知,然后通过服务监控页面查看堆变化情况。

  • OOM自动导出dump文件

其实,翻一翻服务日志,也是能定位到OOM错误日志的。

不过我们在所有服务启动的时候已配置了OOM自动打印二进制dump文件:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/logs/heapdump.hprof 。

如果没有配置,可以通过命令打印:jmap -dump:format=b,file=/app/logs/heapdump/logs/xxxxx.hprof pid 。(推荐oom自动导出dump文件,手动导出的方式在某些情况下已失去快照的意义了吧)

  • 使用jvisualvm分析dump文件

可以通过jdk自带的 jvisualvm.exe 来分析 heapdump.hprof 文件。

到此,已经准确地定位到了相关代码。

OOM触发前,肯定也会频繁触发full gc,可以从full gc的频次和时间来评估服务运行状态。

  • JVM启动参数设置

所以平时部署服务的时候,在JVM启动参数里最好加以下命令:

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/home/admin/logs
-Xloggc:/home/admin/logs/gc.log
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps

如果服务器dump在linux服务器上怎么分析呢?

可以用 jhat 命令来分析,方法很简单,jhat -J-Xmx1024m -port <自定义端口> xxxx.hprof 。

一次线上StackOverflowError记录

背景

使用 split(String regex, int limit) 分割超大json字符串时,引发 java.lang.StackOverflowError 异常。

解决方案

在解决堆内存溢出的时候,可以通过GC日志,dump快照文件来分析解决,可是在gc日志并没有看到因为栈溢出引发的gc。并且在栈溢出的时候,也没有生成dump快照文件,所以这些对栈溢出的问题解决没有太大作用。

其实栈溢出解决,非常简单,只需要通过服务日志来排查就行了,直接可以定位到问题的相关代码了。

本次栈溢出的主要问题是,在使用正则表达式的时候,由于我的字符串是个超级大的 json 字符串,pattern底层是通过递归方式调用执行的,每一层的递归都会在栈线程的大小中占一定内存,如果递归的层次很多,就会报出stackOverFlowError异常。