OOM
OOM是非常严重的问题,除了
程序计数器
,其他内存区域都有溢出的风险。和我们平常工作最密切的,就是堆溢出。另外,元空间在方法区内容非常多的情况下也会溢出。还有就是栈溢出,这个通常影响比较小。堆外也有溢出的可能,这个就比较难排查一些。
1. Java.lang.StackOverflowError
在jvm运行时的数据区域中有一个java虚拟机栈,当执行java方法时会进行压栈弹栈的操作。在栈中会保存局部变量,操作数栈,方法出口等等。jvm规定了栈的最大深度,深度的方法调用,当执行时栈的深度大于了规定的深度,就会抛出StackOverflowError错误。
2. Java.lang.OutOfMemoryError:Java_heap_space
对象太多了、大对象。
3. Java.lang.OutOfMemoryError:GC_overhead_limit_exceeded
GC回收时间过长时会抛出OOM,超过98%的时间用来做GC但是回收了不到2%的堆内存,连续多次GC都只回收了不到2%的极端情况下才会抛出。假如不抛出,那GC清理的那点内存很快会再次填满,迫使GC再次执行,恶性循环,CPU使用率100%,而GC却没有任何效果。
4. Java.lang.OutOfMemoryError:Direct_buffer_memory
Netty写NIo(非阻塞IO)程序经常使用ByteBuffer来读取或者写入数据,这是一种基于通道(Chonnel)与凝冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
ByteBuffer.allocate(capability)第一种方式是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢,
ByteBuffer.allocteDirect(capability)第二种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快。
但如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象们就不会被回收,这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemoryError,那程序就直接崩溃了。
5. Java.lang.OutOfMemoryError:unable_to_create_new_native_thread
1) 导致原因:
a. 一个应用进程创建太多线程了,超过系统承载极限。
b. 服务器不允许你的应用程序创建这么多线程,linux系统默认允许单个进程可以创建的线程数是1024个,你的应用创建超过这个数量,就会报错。
2) 解决办法:
a. 想办法降低你应用程序创建线程的数量,分析应用是否真的需要创建这么多线程,如果不是,改代码将线程数降到最低。
b. 对于有的应用,确实需要创建很多线程,远超过Linux系统的默认1024个线程的限制,可以通过修改linux服务器配置,扩大Linux默认限制。
6. Java.lang.OutOfMemoryError:Metaspace
Java8后使用Metaspace代替永久代。Metaspace是方法区在HotSpot中实现,区别:Metaspace并不在JVM内存中而是使用本地内存。存放的信息:JVM加载的类信息、常量池、静态变量、即时编译后的代码。
常见问题:
你都有哪些手段用来排查内存溢出?
内存溢出包含很多种情况,我在平常工作中遇到最多的就是
堆溢出
。有一次线上遇到故障,重新启动后,使用jstat命令,发现Old区在一直增长。我使用jmap命令,导出了一份线上堆栈,然后使用
MAT
进行分析。通过对
GC Roots
的分析,我发现了一个非常大的HashMap对象,这个原本是有位同学
做缓存
用的,但是一个无界缓存,造成了堆内存占用一直上升。后来,将这个缓存改成 guava的Cache,并设置了弱引用,故障就消失了。
什么情况下会发生栈溢出?
栈的大小可以通过-Xss参数进行设置,当递归层次太深的时候,就会发生栈溢出。比如循环调用,递归等。
什么情况会造成元空间溢出?
元空间(Metaspace)默认是没有上限的,不加限制比较危险。当应用中的Java类过多,比如Spring等一些使用动态代理的框架生成了很多类,如果占用空间超出了我们的设定值,就会发生元空间溢出。 所以,默认风险大,但如果你不给足它空间,它也会溢出。
什么时候会造成堆外内存溢出?
使用了Unsafe类申请内存,或者使用了JNI对内存进行操作。这部分内存是不受JVM控制的,不加限制的使用,容易发生内存溢出。
有什么堆外内存的排查思路?
进程占用的内存,可以使用top命令,看RES段占用的值。如果这个值大大超出我们设定的最大堆内存,则证明堆外内存占用了很大的区域。 使用gdb可以将物理内存dump下来,通常能看到里面的内容。更加复杂的分析可以使用perf工具,或者谷歌开源的gperftools。那些申请内存最多的native函数,很容易就可以找到。
当出现了OOM,怎么排错?
- 首先控制台查看错误日志;
- 然后使用jdk自带的工具查看系统的堆栈日志;
- 定位出内存溢出的空间:堆、栈、永久代(jdk8以后不会出现永久代的内存溢出:被废弃,因为永久代内存经常不够用或者发生内存泄露,永久代会为GC带来不必要的复杂度);
- 如果是堆内存溢出,看是否创建了很大的对象;
- 如果是栈内存溢出,看是否创建了很大的对象,或者产生了死循环,或者引用了较大的全局变量
排查 OOM 的方法:
- 增加两个参数 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息到指定目录;
- 同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域;
- 使用 MAT 工具载入到 dump 文件,分析大对象的占用情况,比如 HashMap 做缓存未清理,时间长了就会内存溢出,可以把改为弱引用 。
你遇到过哪些 OOM 的案例?
首先说说栈溢出吧,这个问题是在方法进行递归调用的时候,调用的深度超过了虚拟机栈的最大深度,所导致的错误。当时在排查时,可以知道调用的接口,在debug的过程找到了出错的方法位置。再查询其递归调用的条件,发现原来是因为数据异常,导致循环调用。然后对相应数据做了修复,并对递归方法加了一些判断的逻辑,进行容错,防止因为数据问题再次产生这种错误。
再说说堆溢出吧,这个问题是因为我们线上有些服务的实例会莫名其妙的宕机重启或者是健康检查没有通过,平常一般试用JDK自带的工具jvisualvm来查看内存的各项信息,再dump快照文件。线上的话可以用 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域,然后用jmap命令,导出了一份线上堆栈。不过我们线上出现问题,一般都是由DBA把dump好的文件发给我们进行分析,分析用的MAT软件,发现原来是因为文件流或者是线程池使用不规范造成的,使用完没有显示的关闭,导致内存不断的增长,最终服务被打死。解决方法就是对所有使用到的地方进行显示的关闭,并在部门内做好规约,不要自己显示的创建线程池,而是用架构师提供的公共线程池。
还有一次是因为从数据库查询大结果集,数据加载到内存处理,引起了频繁的full gc。最后发现是因为在select语句中where条件因为传入了null值导致没有控制住,导致进行了全表扫描。
除了栈堆,还有元空间、堆外内存等地方会发生OOM,看过一些其他人的案例,但是我个人在工作中还未遇到过。