JAVA系统除了程序计数器和虚拟机内存之外的其它几个内存区域都有发生OutOfMemory(OOM)的可能。堆,栈,方法区,静态常量池,直接内存,都是可能的。
1.Java堆溢出
Java堆用于存储对象实例,只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在堆达到最大容量的限制后就会产生内存溢出异常。
-Xms -Xmx 参数可以设置Java堆的大小(实际使用中一般Xms和Xmx的大小一致,放置堆自动扩展),通过参数-XX:+HeapDumpOnOutOfMemoryError让虚拟机在出现内存溢出的异常时Dump出当前的内存堆转储快照以便事后进行分析。
当Java出现堆溢出的时候,异常的堆栈信息为“java.lang.OutOfMemberError” 后会跟着进一步提示“java heap space”
下面有一个堆溢出的OOM例子:
/** * -verbose:gc -Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError * -XX:+PrintGCDetails -XX:SurvivorRatio=8 * @author scl * */ public class HeapOOM { static class OOMObject { } public static void main(String[] args) { List<OOMObject> list = new ArrayList<OOMObject>(); while(true){ list.add(new OOMObject()); } } }
下方为执行结果:
可以看到出现了一个堆溢出,并产生了一个dump
要解决这个区域的异常需要借助内存映像分析工具对转储快照进行分析,确认内存中的对象是否是必要的。也就是确认到底出现了内存泄露(Member Leak)还是内存溢出(Member Overflow)。
如果存在内存泄露,可以通过内存分析工具查看泄露的对象到GC Roots的引用.--暂时不理解如何查看引用
如下,可以推测出16M的空间被一个Object持有,对应的该对象内部都是OOMObject构成的,很自然的联想到List<OOMObject> list = new ArrayList<OOMObject>();对象.
如果是内存溢出那么就需要检查-Xms 和-Xmx大小,本机物理内存大小,或者其他调优。
2.虚拟机栈和本地方法栈溢出
栈容量由-Xss参数设定,以下是测试案例:
/** * -verbose:gc -Xms20M -Xmx20M -Xss128K * -XX:+PrintGCDetails -XX:SurvivorRatio=8 * @author scl * */ public class JavaVMStackSOF { private int stackInteger = 1; public void stackLeak(){ stackInteger++; stackLeak(); } public static void main(String[] args) throws Throwable{ JavaVMStackSOF oom = new JavaVMStackSOF(); try{ oom.stackLeak(); }catch (Throwable e) { // TODO: handle exception System.out.println("stack length :"+oom.stackInteger); throw e; } } }
下方是结果截图:
这里顺便提一下本来我在这里异常捕获的时候使用的是Exception ,结果不会打印出stack length 所以表示这个StackOverflowError不是Exception。而使用Throwable即可捕获并打印出栈深度。
这里测试使用的是单线程的情况,如果是多线程的情况下,每个栈分配的内存越大,反而更容易产生内存溢出异常。原因为栈的大小为:操作系统内存-Xmx(最大堆容量)-MaxPermSize(最大方法区容量)-程序计数器(可以忽略不计)。所以在操作系统内存恒定的情况下,每个线程分配的栈容量越大,可以创建的线程数就越少,对应的创建一个新的线程就越容易发生内存溢出。当系统内存溢出又无法减少线程数或者加大总内存的情况下可以尝试减少栈的-Xss值创建新的线程。
/** * -verbose:gc -Xms20M -Xmx20M -Xss2M * @author scl * */ public class JavaVMStackOOM { private void dontStop(){ while(true){ } } public void stackLeakByThread(){ while(true){ Thread t = new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub dontStop(); } }); t.start(); } } public static void main(String[] args) { JavaVMStackOOM oom = new JavaVMStackOOM(); oom.stackLeakByThread(); } }
以上代码执行具有风险,会造成操作系统假死,如果想要尝试请先保存当前所有文件。
3.方法区和内存常量池溢出
由于内存常量池包含在方法区内部,所以可以使用-XX:PermSize和-XX:MaxPermSize限制方法区大小,从而间接限制内存常量池的大小。
/** * -XX:PermSize=10M -XX:MaxPermSize=10M * @author scl * */ public class RuntimeConstantPoolOOM { public static void main(String[] args) { List<String> list = new ArrayList<String>(); int i = 0; while(true){ list.add(String.valueOf(i++).intern()); } } }
异常信息如下
Sting. Intern方法是一个Native方法,如果字符串常量池中已经包含一个等于该String对象的字符串,则返回代表池中这个字符串的String对象,否则将此String对象添加到常量池中,并返回此String对象的引用.此处OOM后跟着PermGen space也证明了HotSpot的常量池属于方法区的一部分。
4.本机直接内存溢出--没有完成测试
DircetMemory容量可以通过-XX:MaxDirectMemorySize指定,如果不指定则与JAVA堆的最大值(-Xmx)一样。
DircetMemory造成的内存溢出,一个明显的特征是Heap Dump文件会看不到任何明显的异常,如果发现OOM之后的dump文件很小,那么可以考虑是不是这方面的问题。