在Java运行时数据区中,除了Program Counter Register(程序计数器)之外,其他几个数据区中均可能发生OutOfMemoryError,俗称OOM。
1.Java堆溢出
堆主要是用于对象创建时内存的分配,只要我们不断创建对象,并且这些对象在GC时不会被回收掉,则会发生Java堆内存溢出。我们通过以下程序来模拟Java堆溢出的情况:
首先配置虚拟机参数,将堆大小设置为20M并且不可扩展:
-verbose:gc -Xms20M -Xmx20M -XX:+PrintGCDetails
public class Test {
public static class Obj {
}
public static void main(String[] args) throws Exception {
List<Obj> list = new ArrayList<Obj>();
while(true) {
//循环创建对象,并且对象并不能被回收
list.add(new Obj());
}
}
}
运行结果如下:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:2245)
at java.util.Arrays.copyOf(Arrays.java:2219)
at java.util.ArrayList.grow(ArrayList.java:242)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208)
at java.util.ArrayList.add(ArrayList.java:440)
at Test.main(Test.java:13)
Java堆内存溢出是最常见的内存溢出情况,出现Java堆的OOM时,异常栈信息一般会有java.lang.OutOfMemoryError: Java heap space。
2.虚拟机栈溢出
当线程执行一个方法时,java虚拟机会创建一个新的栈帧(stack frame),并压入到该线程的栈中,只要这个方法没有返回,这个栈帧就一直存在于栈中。如果方法嵌套调用层次太多(如上面的递归调用),则该线程栈中的栈帧会越来越多,并且方法都没有结束返回,这些栈帧也不会出栈,最终导致这个线程的栈中所有栈帧的大小总和大于-Xss设置的值,从而产生StackOverflowError异常。
本地方法栈与虚拟机栈类似,在这里会有2种溢出情况:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,则抛出StackOverflowError异常;
- 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
我们来模拟栈溢出情况,首先配置虚拟机参数,设置栈大小为160k:
-verbose:gc -Xss160k -XX:+PrintGCDetails
public class Test {
private int l = 1;
public void callMethod() {
System.out.println(l);
l++;
callMethod();
}
public static void main(String[] args) throws Exception {
Test t = new Test();
try {
t.callMethod();
} catch(Exception e) {
System.out.println("stack length : " + t.l);
throw e;
}
}
}
运行结果如下:
Exception in thread "main" java.lang.StackOverflowError
at Test.callMethod(Test.java:11)
at Test.callMethod(Test.java:12)
at Test.callMethod(Test.java:12)
......
......
在单线程下测试以上代码时,发现只会抛出StackOverflowError异常。
3.方法区的溢出
方法区用于存放Class信息、常量等,方法区的内存溢出主要有2种情况:
- 现在的很多主流框架如Spring,会在运行时生成大量的增强类,增强的类越多,就需要越大的方法区来保存动态生成的Class信息,因此也越容易发生内存溢出。
- 运行时常量池内存溢出。
我们可以使用String.intern()方法不断生成字符串常量来模拟内存溢出,同样先设置方法区的内存大小。
-verbose:gc -XX:PermSize=4M -XX:MaxPermSize=4M -XX:+PrintGCDetails
public class Test {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
int i = 0;
while(true) {
System.out.println(i);
list.add(String.valueOf(i++).intern());
}
}
}
运行结果如下:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
......
从异常栈信息中可以看到,在java.lang.OutOfMemoryError后面跟随的是"PermGen space",这里说明常量池是属于虚拟机中永久代的一部分。
4.小结
本文主要模拟了不同的内存区域出现内存溢出的情况,当我们自己的应用程序出现内存溢出时,就能根据异常信息定位到内存溢出的代码所在。
网友评论