除了程序计数器外,Java虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError异常的可能.
本节内容的目的:
1.通过代码验证各个运行时区域存储的内容;
2.希望读者在遇到实际的内存溢出时,能够快速根据异常信息判断出那个区域的内存溢出及知道该如何处理.
虚拟机参数设置:
一 Java堆溢出:
1.1 异常构建
示例代码:
/** * 测试堆内存溢出 */ public class HeapOOM { static class OOMObject{ } public static void main(String[] args) { List<OOMObject> list = new ArrayList<OOMObject>(); while (true) { list.add(new OOMObject()); } } }
运行结果:
java堆内存异常是最常见的内存溢出情况,当出现Java堆内存溢出时,OutOfMemoryError会进一步提示: Java heap space.
1.2 异常的解决:
1.2.1 先通过内存映像分析工具,对Dump出来的堆转储快照进行分析,确认内存中的对象是否是必须的;分析出内存泄漏还是内存溢出.
这里使用的eclipse的MAT工具.下图为MAT打开的堆转储快照文件:
如果是内存泄漏,可已通过工具查看对象的GC Roots的引用链.就能找到泄漏的对象是通过怎样的路径的引用导致GC无法回收他们的.掌握到泄漏对象的类信息和GC Roots引用链,就可以很准确的定位到泄漏的代码.
如果不存在泄漏的话,就是内存中的对象必须存活着,那就应当检查虚拟机堆参数(-Xmx,-Xms),于机器物理内存对比看是否还可以调大,从代码上检查是否存在某些生命周期过长,持有状态时间过长的情况,尝试减少运行时期的内存消耗.
二 虚拟机栈和本地方法栈溢出
由于在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此对于HotSpot来说,虽然-Xoss参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只由-Xss参数设定.关于虚拟机栈和本地方法栈,在Havana虚拟机规范描述了两种异常:
a) 如果线程请求深度超出了虚拟机所允许的最大深度,抛出StackOverflowError异常
b) 如果虚拟机在扩展栈时无法申请到足够的内存空间,抛出OutOfMemoryError异常
2.1 异常构建
/**
* VM Args: -Xss128k * @author win * */ public class JavaVMStackSOF { private int stackLength = 1; public void stackLeak(){ stackLength ++; stackLeak(); } public static void main(String[] args) throws Throwable { JavaVMStackSOF oom = new JavaVMStackSOF(); try { oom.stackLeak(); } catch (Throwable e) { System.out.println("stack length :" + oom.stackLength); throw e; } } }运行结果:
使用-Xss参数减小栈内存容量,结果抛出StackOverflowError异常,异常出现时输出的栈深度响应缩小.
定义大量的本地变量,增加此方法帧中本地变量表的长度,结果抛出StackOverflowError异常,异常出现时输出的栈深度响应缩小.
在单线程情况下.无论是栈帧太大还是虚拟机容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常.
不限于单线程的情况下,不断地创建线程就可以产生内存溢出的异常:
/** * VM args: -Xss2M * @author win * */ public class JavaVMStackOOM { private void dontStop(){ while (true){} } public void stackLeakByThread(){ while(true){ Thread thread = new Thread(new Runnable(){ public void run() { dontStop(); } }); thread.start(); } } public static void main(String[] args) { JavaVMStackOOM oom = new JavaVMStackOOM(); oom.stackLeakByThread(); } }
(由于在Windows平台的虚拟集中,Java的线程是映射到操作系统的内核线程上的,因此上述代码执行时有较大的风险,可能会导致操作系统假死.执行前记得先保存当前的工作)
分析:
操作系统分配给每个进程的内存是有限制的.虚拟机提供了参数来控制Java堆和方法区这两部分内存的最大值.程序计数器消耗内存很小可以忽略不计,如果虚拟机本身消耗的内存不计算在内,那么剩下的内存就由虚拟机栈和本地方法栈瓜分.每个线程分配到栈容量越大,则可以建立的线程数量自然就越少,建立线程是就越容易吧剩下的内存耗尽.
所以这种情况产生内存溢出异常和栈空间是否足够大并不存在任何联系.
2.2 分析
如果出现StackOverflowError异常时,有错误堆栈可以阅读,比较容易找到问题所在.如果使用虚拟机默认的参数,栈深度在大剁手情况下完全够用.如果建立多线程的情况下出现内存溢出,在不能减少线程数量和更换64位虚拟机的情况下,只能通过减少最大堆内存和减少栈容量来换取更多的线程.