原创 面试官:哪些场景会产生OOM?怎么解决?

发布时间:2021-06-24 08:44:58 浏览 4530 来源:猿笔记 作者:艾小仙

    堆内存用来存储对象实例,34###方法区(运行时常量池)和元空间溢出,包含Class文件信息、运行时常量池、常量池。运行时常量池和常量池的主要区别是具备动态性,也就是不一定非要是在Class文件中的常量池中的内容才能进入运行时常量池。运行期间也可以可以将新的常量放入池中,如果字符串常量池中已经包含一个等于此String对象的字符串,将此String对象包含的字符串添加到常量池中。所以会报出堆内存溢出的错误。直接内存并不是虚拟机运行时数据区域的一部分,常见的比如在NIO中可以使用native函数直接分配堆外内存就容易导致OOM的问题。


    这个面试问题是一个朋友在面试的时候遇到的。什么时候会抛出OutOfMemery异常?乍一看,好像挺简单的。其实就是对整个JVM的理解,网上可以找到一些乱七八糟的答案。其实基本上可以总结出四种场景。

    # # #堆内存溢出

    堆内存溢出太常见了,大部分人都应该想到。堆内存用于存储对象实例。只要我们继续创建对象,并确保GCRoots和对象之间有一个可到达的路径,以避免垃圾收集,在对象数量超过最大堆大小限制后,就会很快出现这种异常。

    编写一段代码进行测试,并将堆内存大小设置为2M。

    javapublicclassHeapOOM{publicstaticvoidmain(String[]args){Listlist=newArrayList<>();while(true){list.add(newHeapOOM());}}}

    运行代码,很快就可以看到OOM异常。这里的提示是*Javaheapspace*堆内存溢出。

    一般的排查方式可以通过设置-XX:+HeapDumpOnOutOfMemoryError在发生异常时dump出当前的内存转储快照来分析,分析可以使用EclipseMemoryAnalyzer(MAT)来分析,独立文件可以在[官网](

    另外如果使用的是IDEA的话,可以使用商业版JProfiler或者开源版本的JVM-Profiler,此外IDEA2018版本之后内置了分析工具,包括FlameGraph(火焰图)和CallTree(调用树)功能。

    # # #方法区(运行时常量池)和元空间溢出

    像堆一样,方法区域由线程共享,包括类文件信息、运行时常量池和常量池。运行时常量池和常量池的主要区别在于它是动态的,即不一定要在Class文件中的常量池中才能实习运行时常量池,运行时也可以将新的常量放入池中,比如String的int()方法。

    我们写一段代码验证一下String.intern(),同时我们设置-XX:MetaspaceSize=50m-XX:MaxMetaspaceSize=50m元空间大小。由于我使用的是1.8版本的JDK,而1.8版本之前方法区存在于永久代(PermGen),1.8之后取消了永久代的概念,转为元空间(Metaspace),如果是之前版本可以设置PermSizeMaxPermSize永久代的大小。

    javaprivatestaticStringstr="test";publicstaticvoidmain(String[]args){Listlist=newArrayList<>();while(true){Stringstr2=str+str;str=str2;list.add(str.intern());}}

    当您运行代码时,您会发现代码报告了一个错误。

    再次修改配置,取消元空间限制,修改堆内存大小-xm20m-xmx20m。您可以看到堆内存报告了一个错误。

    这是为什么?Intern()本身就是一个原生方法,它的作用是:如果string常量池已经包含了一个与这个String对象相等的String,则返回池中表示这个String的String对象;否则,该字符串对象中包含的字符串将被添加到常量池中,并返回对该字符串对象的引用。

    1.7版以后字符串常量池已经转移到堆区,所以会报告堆内存溢出的错误。如果是1.7版之前,会看到PermGenspace报告的错误。

    # # #直接内存溢出

    直接内存不是虚拟机运行时数据区的一部分,不受堆内存限制,受机器内存大小限制。常见的,比如在NIO中,本机函数可以用来直接分配堆外内存,容易导致OOM问题。

    直接内存大小可以通过-XX:MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值-Xmx一样。

    直接内存导致内存溢出的一个明显特征是在Dump文件中看不到明显的异常。如果在程序中直接或间接使用OOM和NIO后发现Dump文件很小,可以考虑检查一下是否是这个原因。

    # # #堆栈内存溢出

    栈是线程私有的,它的生命周期和线程一样。当执行每个方法时,创建一个堆栈框架来存储局部变量表、操作数堆栈、动态链接、方法出口和其他信息。方法调用的过程就是栈帧进入和退出的过程。

    在java虚拟机规范中,为虚拟机堆栈定义了两个例外:

    1.如果线程请求的堆栈深度大于虚拟机允许的深度,将引发堆栈溢出错误

    2.如果虚拟机堆栈可以动态扩展,并且在扩展期间无法应用足够的内存,则会引发OutOfMemoryError异常

    先写一段代码测试一下,set -Xss160k,-Xss表示每个线程的栈内存大小

    javapublicclassStackOOM{privateintlength=1;publicvoidstackTest(){System.out.println("stacklenght="+length);length++;stackTest();}publicstaticvoidmain(String[]args){StackOOMtest=newStackOOM();test.stackTest();}}

    测试表明,无论如何在单线程下设置参数,都是StackOverflow异常。

    尽量把代码修改成多线程,调整-Xss2m,因为分配给每个线程的内存越大,堆栈空间能容纳的线程数越少,越容易出现内存溢出。相反,如果内存不足,可以减少参数以支持更多线程。

    javapublicclassStackOOM{privatevoiddontStop(){while(true){}}publicvoidstackLeakByThread(){while(true){newThread(()->dontStop()).start();}}publicstaticvoidmain(String[]args)throwsThrowable{StackOOMstackOOM=newStackOOM();stackOOM.stackLeakByThread();}}

作者信息

艾小仙 [等级:3] 公号:艾小仙
发布了 57 篇专栏 · 获得点赞 642 · 获得阅读 41005

相关推荐 更多