当前位置: 首页 > 科技观察

JVM实战OutOfMemoryError异常

时间:2023-03-12 02:33:46 科技观察

在《Java虚拟机规范》的规定中,除了程序计数器之外,虚拟机内存的其他几个运行时区域都有可能出现OutOfMemoryError(以下简称OOM)异常。(本文主要基于jdk1.8来讨论)Java堆溢出Java堆是用来存放对象实例的。我们只需要不断地创建对象,保证GCRoots和对象之间有可达的路径,避免垃圾回收机制清除这些对象。那么随着对象数量的增加,总容量达到最大堆容量限制,就会出现内存溢出异常。下面的模拟代码是堆内存溢出代码的简单模拟:/***VMArgs:-Xms10m-Xmx10m-XX:+HeapDumpOnOutOfMemoryError*@authorzhengsh*@date2021-8-13*/publicclassHeapOOM{publicstaticvoidmain(String[]args){Listlist=newArrayList<>();while(true){list.add(newbyte[2048]);}}}返回结果信息如下:Exceptioninthread"main"java.lang.OutOfMemoryError:javaheapspaceatcn.zhengsh.jvm.oom.HeapOOM.main(HeapOOM.java:16)问题分析我们需要定位内存泄漏(MemoryLeak)或者,内存溢出(MemoryOverflow)内存泄漏内存溢出内存泄漏我们可以使用jdk自带的jvisualvm工具加载堆快照文件进行分析。如果是内存泄漏,可以进一步使用工具查看泄漏对象到GCRoots的引用链,找出泄漏对象关联的引用路径和哪些GCRoots,这样垃圾回收器就无法回收了他们。根据泄漏对象的类型信息及其引用链信息给GCRoots,一般可以比较准确地定位到这些对象创建的位置,进而找出产生内存泄漏的代码的具体位置。如果内存溢出不是内存泄漏,换句话说,内存中的对象必须存活,那么你应该检查Java虚拟机的堆参数(-Xmx和-Xms)设置,并与机器的内存进行比较看是否有向上调整的空间。然后从代码中检查是否存在一些对象生命周期过长、保持状态时间过长、存储结构设计不合理等,尽量减少程序运行时的内存消耗。虚拟机栈和本地方法栈溢出HotSpot虚拟机不区分虚拟机栈和本地方法栈,所以对于HotSpot来说,虽然-Xoss参数(设置本地方法栈大小)存在,但实际上并没有作用,堆栈容量只能通过-Xss参数设置。关于虚拟机栈和本地方法栈,在《Java虚拟机规范》中描述了两个异常:如果线程请求的栈深度大于虚拟机允许的最大深度,则会抛出StackOverflowError异常。如果虚拟机的栈内存允许动态扩展,当扩展后的栈容量无法申请到足够的内存时,会抛出OutOfMemoryError异常。《Java虚拟机规范》很明显Java虚拟机是允许选择是否支持栈动态扩展的,而HotSpot虚拟机是不支持扩展的,所以除非在创建时因为无法获取足够的内存而出现OutOfMemoryError异常一个线程去申请内存,否则在线程运行的时候,不会因为扩容导致内存溢出。它只会导致StackOverflowError异常,因为堆栈容量无法容纳新的堆栈帧。虚拟机栈内存溢出StackOverflowError示例代码:/***VMArgs:-Xss128k**@authorzhengsh*@date2021-08-13*/publicclassJavaVMStackSOF{privateintstackLength=1;publicvoidstackLeak(){stackLength++;stackLeak();}publicstaticvoidmain(String[]args)throwsThrowable{JavaVMStackSOFoom=newJavaVMStackSOF();try{oom.stackLeak();}catch(Throwablee){System.out.println("stacklength:"+oom.stackLength);throwe;}}}返回异常信息Exceptioninthread"main"java.lang.StackOverflowErrorstacklength:992atcn.zhengsh.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)atcn.zhengsh.jvm.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:14)//....省略更多OutOfMemoryErrorpackagecn.zhengsh.jvm.oom;/***@authorzhengsh*@date2021-08-13*/publicclassJavaVMStackSOF2{privatestaticintstackLength=0;publicstaticvoidtest(){longunused1,unused2,unused3,unused4,unused5,unused6,unused7,unused8,unused9,unused10,unused11,unused12,unused13,unused14,unused15,unused16,unused17,unused18,unused19,unused20,unused21,unused22、未使用23、未使用24、未使用25、未使用26、未使用27、未使用28、未使用29、未使用30、未使用31、未使用32、未使用33、未使用34、未使用35、未使用36、未使用37、未使用38、未使用39、未使用40、未使用41、未使用34、未使用4、未使用4、未使用4、未使用4、未使用42、未使用4、未使用4、未使用4unused47,unused48,unused49,unused50,unused51,unused52,unused53,unused54,unused55,unused56,unused57,unused58,unused59,unused60,unused61,unused62,unused63,unused64,unused65,unused66,unused67,unused68,unused69,unused70,unused71,unused72,unused73,unused74,unused75,unused76,unused77,unused78,unused79,unused80,unused81,unused82,unused83,unused84,unused85,unused86,unused87,unused88,unused89,unused90,unused91,unused92,unused93,unused94,unused95,unused96,unused97,unused98,unused99,unused100;stackLength++;test();unused1=unused2=unused3=unused4=unused5=unused6=unused7=unused8=unused9=unused10=unused11=unused12=unused13=unused14=unused15=unused16=unused18unused19=unused20=unused21=unused22=unused23=unused24=unused25=unused26=unused27=unused28=unused29=unused30=unused31=unused32=unused33=unused34=unused35=unused36=unused37=unused38=unused39=unused40=unused41=unused42=unused43=unused44=unused45=unused46=unused47=unused48=unused49=unused50=unused51=unused52=unused53=unused54=unused55=unused56=unused57=unused58=unused59=unused60=unused61=unused62=unused63=unused64=unused65=unused66=unused67=unused68=unused69=unused70=unused71=unused72=unused73=unused74=unused75=unused76=unused77=unused78=unused79=unused80=unused81=unused82=unused83=unused84=unused85=unused86=unused87=unused88=unused89=unused90=unused91=unused92=unused93=unused94=unused95=unused96=unused97=unused90=unused90=unused90=unused90(String[]args){try{test();}catch(Errore){System.out.println("stacklength:"+stackLength);throwe;}}}输出结果:stacklength:6986Exceptioninthread"main"java.lang.StackOverflowErroratcn.zhengsh.jvm.oom.JavaVMStackSOF2.test(JavaVMStackSOF2.java:22)atcn.zhengsh.jvm.oom.JavaVMStackSOF2.test(JavaVMStackSOF2.java:22)总结不管是栈帧太大还是虚拟机栈容量太小,当无法分配新的栈帧内存时,HotSpot虚拟机总会抛出StackOverflowError异常.在允许动态扩展堆栈大小的虚拟机上,相同的代码会导致不同的情况。创建线程导致内存溢出注意:下面的实验可能会导致操作系统卡死。建议在虚拟机中执行/***VMArgs:-Xss512k**@authorzhengsh*@date2021-08-13*/publicclassJavaVMStackOOM{privatevoiddontStop(){while(true){}}publicvoidstackLeakByThread(){while(true){Threadthread=newThread(newRunnable(){@Overridepublicvoidrun(){dontStop();}});thread.start();}}publicstaticvoidmain(String[]args)throwsThrowable{JavaVMStackOOMoom=newJavaVMStackOOM();oom。stackLeakByThread();}}方法区和运行时常量池溢出由于运行时常量池是方法区的一部分,所以这两个区域的溢出测试可以放在一起。从JDK7开始,H??otSpot逐渐“去掉了永久代”的计划,在JDK8中完全使用元空间来代替永久代。方法区内存溢出方法区的主要职责是存储类型的相关信息,比如类名、访问修饰符、常量池、字段描述、方法描述等。对于这部分区域的测试,基本思路是在运行时生成大量的类来填充方法区,直到溢出。虽然直接使用JavaSEAPI(如GeneratedConstructorAccessor和反射时的动态代理)可以动态生成类,但在本实验中,大量的动态类是借助CGLib直接操作字节码生成的。/***VMArgs:-XX:MetaspaceSize=21m-XX:MaxMetaspaceSize=21m**@authorzhengsh*@date2021-08-13*/publicclassJavaMethodAreaOOM{publicstaticvoidmain(String[]args){while(true){Enhancerenhancer=newEnhancer();enhancer.setSuperclass(OOMObject.class);enhancer.setUseCache(false);enhancer.setCallback(newMethodInterceptor(){@OverridepublicObjectintercept(Objectobj,Methodmethod,Object[]args,MethodProxyproxy)throwsThrowable{returnproxy.invokeSuper(obj,args);}});enhancer.create();}}staticclassOOMObject{}}输出代码Causedby:java.lang.OutOfMemoryError:MetaspaceCausedby:java.lang.OutOfMemoryError:Metaspace常量池caseString::intern()是native方法,它的作用是如果字符串常量池中已经包含了等于这个String对象的字符串,则返回池中代表该字符串的String对象的引用;否则,这个String对象中包含的字符串将被添加到常量池中,并返回这个String对象的引用。在JDK6之前的HotSpot虚拟机或更早版本中,常量池分配在永久代。我们可以通过-XX:PermSize和-XX:MaxPermSize来限制永久代的大小,可以间接限制常量池的容量。./***@authorzhengsh*@date2021-08-13*/publicclassRuntimeConstantPoolOOM2{publicstaticvoidmain(String[]args){Stringstr1=newStringBuilder("computer").append("software").toString();System.out.println(str1.intern()==str1);Stringstr2=newStringBuilder("ja").append("va").toString();System.out.println(str2.intern()==str2);}}这个如果代码在JDK6中运行,它会得到两个false,但在JDK7中,它会得到一个true和一个false。之所以不同是因为在JDK6中,intern()方法会将第一次遇到的字符串实例复制到永久代的字符串常量池中进行存储,并返回永久代中字符串实例的引用,whileStringBuilder创建的字符串对象实例在Java堆上,所以肯定不是同一个引用,结果会返回false。JDK7(以及其他一些虚拟机,如JRockit)的intern()方法的实现不需要将字符串的实例复制到永久代。由于字符串常量池已经移到了Java堆中,所以只需要在常量池中记录第一次出现的实例引用即可,所以intern()返回的引用与字符串相同StringBuilder创建的实例。str2的比较返回false,因为在执行String-Builder.toString()之前就已经出现了字符串"java",并且字符串常量池中已经有对它的引用,不符合要求intern()方法“第一次遇到”的原则,字符串“computersoftware”第一次出现,所以结果返回true。机器直接内存(DirectMemory)的容量可以通过-XX:MaxDirectMemorySize参数指定。默认与Java堆的最大值一致(由-Xmx指定)。代码通过DirectByteBuffer类,直接通过反射获取到Unsafe实例。内存分配(Unsafe类的getUnsafe()方法指定只有bootstrap类加载器才会返回实例,体现了设计者希望只有虚拟机标准类库中的类才能使用Unsafe函数,以及Unsafe部分只会在JDK10中包含函数,通过VarHandle对外开放),因为DirectByteBuffer虽然会分配内存并抛出内存溢出异常,但是当它抛出异常时,实际上并没有向操作系统申请内存allocation,但是它知道内存不能通过计算来分配。代码中手动抛出溢出异常,真正申请内存分配的方法是Unsafe::allocateMemory()。/***VMArgs:-Xmx20M-XX:MaxDirectMemorySize=10M**@authorzhengsh*@date2021-08-13*/publicclassDirectMemoryOOM{privatestaticfinalint_1MB=1024*1024;publicstaticvoidmain(String[]args)throwsException{FieldunsafeField=Unsafe.class.getDeclaredFields()[0];unsafeField.setAccessible(true);Unsafeunsafe=(Unsafe)unsafeField.get(null);while(true){unsafe.allocateMemory(_1MB);}}}输出内容:Exceptioninthread"main"java.lang.OutOfMemoryErrorExceptioninthread"main"java.lang.OutOfMemoryErroratjava.base/jdk.internal.misc.Unsafe.allocateMemory(Unsafe.java:616)atjdk.unsupported/sun.misc.Unsafe.allocateMemory(Unsafe.java:462)atcn。zhengsh.jvm.oom.DirectMemoryOOM.main(DirectMemoryOOM.java:21)参考资料《深入理解 JVM 虚拟机-第三版》周志明https://docs.oracle.com/javase/specs/jls/se8/html/index.html