针对目前大家对OOM的类型不太熟悉,那么来总结一下各种OOM出现的情况以及解决方法。把各种OOM的情况列出来,然后逐一进行代码编写复现和提供解决方法。
1. 堆溢出-java.lang.OutOfMemoryError: Java heap space。
2. 栈溢出-java.lang.OutOfMemorryError。
3. 栈溢出-java.lang.StackOverFlowError。
4. 元信息溢出-java.lang.OutOfMemoryError: Metaspace。
5. 直接内存溢出-java.lang.OutOfMemoryError: Direct buffer memory。
6. GC超限-java.lang.OutOfMemoryError: GC overhead limit exceeded。
0x01: 堆溢出异常,相信大家很常见。即堆内对象不能进行回收了,堆内存持续增大,这样达到了堆内存的最大值,数据满了,所以就出来了。我们直接放溢出的代码样例。需要设置好idea的VM Options: -Xmx100m,这样设置为最大堆内存,这样运行起来就很快就出来错误了。
package oom;import java.util.ArrayList;import java.util.List;import java.util.concurrent.TimeUnit;public class HeapOOM { static class OOMObject { } public static void main(String[] args) throws InterruptedException { List list = new ArrayList<>(); while(true) {// TimeUnit.MILLISECONDS.sleep(1); list.add(new OOMObject()); } }}
运行的异常如下,代码直接就出来我们看到的异常了。
/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/bin/java -Xmx100mException in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOf(Arrays.java:3210) at java.util.Arrays.copyOf(Arrays.java:3181) at java.util.ArrayList.grow(ArrayList.java:261) at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:235) at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:227) at java.util.ArrayList.add(ArrayList.java:458) at oom.HeapOOM.main(HeapOOM.java:21)Process finished with exit code 1
细心的小伙伴可以发现代码中设置了一个休眠,目的是看一下堆内存的结构和数据图。将休眠代码打开,然后打开JDK自带的jconsole命令,连接上之后看一下概览图,通过下图发现堆内存持续不断的增长。
![f055c8fe2140ca97082ba27f3bf78462.png](https://i-blog.csdnimg.cn/blog_migrate/c1412fdacd11af66ffba6025edbed9ab.jpeg)
打开内存界面,看一下内存,然后点一下GC按钮,这个时候会有一些类进行回收,但是还是会继续增长,看一下下面的图。
![a26d4d70626b1b4833e323e400b6347a.png](https://i-blog.csdnimg.cn/blog_migrate/05c22c2ae9799fbabdf6c6feebd80911.jpeg)
![1dc7a29e2870a1558ff77b1bc0768e2a.png](https://i-blog.csdnimg.cn/blog_migrate/4999a8b8698ce50b9bf302b10bc00dc5.jpeg)
点开信息标签看一下。经过几次GC回收之后,类的数据量还是变化不大,说明没有进行回收。
![711aaa162e3db205fbd001c5176a8fbb.png](https://i-blog.csdnimg.cn/blog_migrate/f33de0faa94146d46d5ac4ac44cf539c.jpeg)
以上这种情况的解决方法就是找到问题点,分析哪个地方是否存储了大量类没有被回收的情况,通过JMAP命令将线上的堆内存导出来后进行分析。
0x02: 看一下栈溢出的情况,下面的代码就是无限的创建线程,直到没法再创建线程。
package oom;import java.util.concurrent.TimeUnit;/** * @Date 2020-07-18 */public class StackOOM { public static void infiniteRun() { while(true) { Thread thread = new Thread(() -> { while (true) { try { TimeUnit.HOURS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } }); thread.start(); } } public static void main(String[] args) { infiniteRun(); }}
抛出来的异常如下,如果真的需要创建线程,我们需要调整帧栈的大小-Xss512k,默认帧栈大小为1M,如果设置小了,可以创建更多线程。
/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/bin/java -Xss512k Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread at java.lang.Thread.start0(Native Method) at java.lang.Thread.start(Thread.java:717) at oom.StackOOM.infiniteRun(StackOOM.java:24) at oom.StackOOM.main(StackOOM.java:29)Process finished with exit code 130 (interrupted by signal 2: SIGINT)
以上这种情况是帧栈不够用了,如果出现了这种情况,需要了解什么地方创建了很多线程,线上程序需要用jstack命令,将当前线程的状态导出来放到文件里边,然后将文件上传到fastthread.io网站上进行分析。
0x03:看一下栈溢出的另一种情况,这就是栈的StackOverFlow的情况。下面就是一个死循环递归调用。
package oom;/** * @Date 2020-07-18 */public class StackOFE { public static void stackOverFlowErrorMethod() { stackOverFlowErrorMethod(); } public static void main(String[] args) { stackOverFlowErrorMethod(); }}
运行之后出现的错误如下,程序每次递归的时候,程序会把数据结果压入栈,包括里边的指针等,这个时候就需要帧栈大一些才能承受住更多的递归调用。通过-Xss进行设置,上边的例子需要设置小一些,以分配更多的帧栈,这次是一个帧栈需要记录程序数据,所以需要更大的值。
/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/bin/java -Xss2mException in thread "main" java.lang.StackOverflowError at oom.StackOFE.stackOverFlowErrorMethod(StackOFE.java:10) at oom.StackOFE.stackOverFlowErrorMethod(StackOFE.java:10) at oom.StackOFE.stackOverFlowErrorMethod(StackOFE.java:10) at oom.StackOFE.stackOverFlowErrorMethod(StackOFE.java:10) at oom.StackOFE.stackOverFlowErrorMethod(StackOFE.java:10) at oom.StackOFE.stackOverFlowErrorMethod(StackOFE.java:10)
遇到上面的情况下,那么就需要通过jstack将线程数据导到文件进行分析。找到递归的点,如果程序就是需要递归的次数的话,那么这个时候就需要增大帧栈的大小以适应程序。
0x04: 元数据区域溢出,元数据区域也成为方法区,存储着类的相关信息,常量池,方法描述符,字段描述符,运行时产生大量的类就会造成这个区域的溢出。我们运行的时候指定一下元数据区域的大小,设置到idea的VM options里边:-XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=30M。
package oom;import net.sf.cglib.proxy.Enhancer;import net.sf.cglib.proxy.MethodInterceptor;import net.sf.cglib.proxy.MethodProxy;import java.lang.reflect.Method;/** * @Date 2020-07-18 */public class MetaspaceOOM { static class OOMObject{} public static void main(String[] args) { while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { return proxy.invokeSuper(obj, args); } }); enhancer.create(); } }}
运行的结果如下,元数据信息溢出了。这种情况产生的原因有:通过CBLIG大量生成类,导致Meta信息满了;JDK7的时候使用String.intern()不当,会产生大量常量数据;加载大量的jsp以及动态生成jsp文件。需要调整元数据空间的大小,如果调大了之后还出现了这种异常,我们需要分析哪里出现的溢出并fix掉。
/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/bin/java -XX:MetaspaceSizeException in thread "main" java.lang.OutOfMemoryError: Metaspace at net.sf.cglib.core.AbstractClassGenerator.generate(AbstractClassGenerator.java:345) at net.sf.cglib.proxy.Enhancer.generate(Enhancer.java:492) at net.sf.cglib.core.AbstractClassGenerator$ClassLoaderData.get(AbstractClassGenerator.java:114) at net.sf.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:291) at net.sf.cglib.proxy.Enhancer.createHelper(Enhancer.java:480) at net.sf.cglib.proxy.Enhancer.create(Enhancer.java:305) at oom.MetaspaceOOM.main(MetaspaceOOM.java:28)Process finished with exit code 1
0x05: 直接内存溢出,除了使用堆内存外,还可能用直接内存,即堆外内存。NIO为了提高性能,避免在Java Heap和native Heap中切换,所以使用直接内存,默认情况下,直接内存的大小和对内存大小一致。堆外内存不受JVM的限制,但是受制于机器整体内存的大小限制。如下代码设置堆最大内存为128m,直接内存为100m,然后我们每次分配1M放到list里边。
-Xmx128m -XX:MaxDirectMemorySize=100Mpackage oom;import java.nio.ByteBuffer;import java.util.ArrayList;import java.util.List;/** * @Date 2020-07-18 */public class DirectBufferOOM { public static void main(String[] args) { final int _1M = 1024 * 1024 * 1; List buffers = new ArrayList<>(); int count = 1; while (true) { ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1M); buffers.add(byteBuffer); System.out.println(count++); } }}
这个时候,当输出100次的时候,下次再分配的时候会报OOM-Direct buffer memory。
/Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/bin/java -Xmx128m Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory at java.nio.Bits.reserveMemory(Bits.java:694) at java.nio.DirectByteBuffer.(DirectByteBuffer.java:123) at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311) at oom.DirectBufferOOM.main(DirectBufferOOM.java:18)Process finished with exit code 1
这种情况是我们使用直接内存造成溢出,这个时候我们需要检查一下程序里边是否使用的NIO及NIO,比如Netty,里边的直接内存的配置。
0x06: JDK1.6之后新增了一个错误类型,如果堆内存太小的时候会报这个错误。如果98%的GC的时候回收不到2%的时候会报这个错误,也就是最小最大内存出现了问题的时候会报这个错误。如果代码配置了最小最大堆内存都为10m。
-Xmx10m -Xms10mpackage oom;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;/** * @Date 2020-07-18 */public class GCOverheadOOM { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(5); for (int i = 0; i { try { Thread.sleep(10000); } catch (InterruptedException e) { //do nothing } }); } }}
这个创建了一个线程池,如果线程池执行的时候如果核心线程处理不过来的时候会把数据放到LinkedBlockingQueue里边,也就是堆内存当中。这个时候我们需要检查-Xms -Xmx最小最大堆配置是否合理。再一个dump出现当前内存来分析一下是否使用了大量的循环或使用大量内存代码。
以上就是经常遇到的情况,需要针对出现的不同情况进行分析和处理。