JAVA虚拟机之内存溢出
上一章主要写到了对象在内存中的创建、布局以及访问(JAVA虚拟机之对象探秘)。内存空间大小是固定大小的,所以一定会存在内存空间不足,导致我们程序无法正常运行。当我们遇到这些问题该如何下手,该如何分析问题。下面我主要从堆溢出、栈溢出、方法区和运行时常量池这三个方面来分析问题。
一、 堆溢出
Java堆用于存放对象实例,主要不断地创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量达到了最大堆的容量限制之后就会出现内存溢出溢出异常。下面我们举个例子:
设置虚拟机参数:-Xmx20m:最大堆内存, -Xms20m最小堆内存,-XX:+HeapDumpOnOutOfMemoryError:当虚拟机出现内存溢出的时候Dump出当前内存堆存储快照以便后面分析。-XX:HeapDumpPath=e:\log:当运行程序时候发生OOM,将会在e盘log目录下生产堆dump文件。

import java.util.ArrayList; import java.util.List; public class Main { static class OOMObject{ private String o; public OOMObject(String o){ this.o = o; } } public static void main(String[] args) { List<OOMObject> list = new ArrayList<OOMObject>(); int i = 0; while (true){ list.add(new OOMObject("o"+i)); i++; } } } 运行结果: java.lang.OutOfMemoryError: Java heap space Dumping heap to e:\log\java_pid2592.hprof ... Heap dump file created [27580218 bytes in 0.176 secs] Exception 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 Main.main(Main.java:17) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
分析dump文件:
使用jdk自带的VisualVm工具,然后装载运行程序时发生OOM异常时生成的dump文件。



这样就能看到我一共创建了多少个实例对象,和每个对象的值都能看见了。
选择类标签,按类名大小排序,选择最大的char[],然后排序char[]的实例,点进去就能分析哪个地方造成的OOM。

解决方案:增加堆大小,然后测试就不会报OOM异常了。但是这种方式不是最终解决OOM异常的根本方法,我们应该优化存大对象的方案。
二、 栈溢出
Java虚拟机栈规范中描述了两种异常,第一种如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverFlowError异常。第二种如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMomeryError异常。
public class VmStackOf { private int stackLength = 1; public void stackLeak(){ stackLength++; stackLeak(); } public static void main(String[] args) { VmStackOf oom = new VmStackOf(); try { oom.stackLeak(); }catch (Exception e){ System.out.println("stack length:"+oom.stackLength); throw e; } } }
Exception in thread "main" java.lang.StackOverflowError at VmStackOf.stackLeak(VmStackOf.java:8) at VmStackOf.stackLeak(VmStackOf.java:8) at VmStackOf.stackLeak(VmStackOf.java:8) at VmStackOf.stackLeak(VmStackOf.java:8) ........
三、 方法区和运行时常量池溢出
由于运行时常量池是方法区的一部分,因此这两个区域就放在一起分析。方法区用于存放class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。这些区域的测试,基本思路是运行时产生大量的类去填满方法区,直到溢出。
文章作者介绍:
来自于小豹科技的李维-公司专注于软件基础研发平台,目前公司正在研发一款基于Netty的插件式的API网关-小豹API网关。 希望与对OpenAPI、微服务、API网关、Service Mesh等感兴趣的朋友多交流。 有兴趣的朋友请加QQ群244054462。

浙公网安备 33010602011771号