在Java虚拟机规范的描述中,除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生 OutOfMemoryError(下文称OOM)异常的可能,本文将通过若干实例来验证异常发生的场景,并且会初步介绍几个与内存相关的最基本的虚拟机参数。
1、Java堆溢出
Java堆用于存储对象实例,只要不断地创建对象,并且保证 GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。
限制Java堆的大小为20MB,不可扩展(将堆的最小值Xms参数与最大值Xmx参数设置为一样即可避免堆自动扩展),通过参数-XX:+HeapDumpOnOutOfMemoryError可以让虚拟机在出现内存溢出异常时Dmp出当前的内存堆转储快照以便事后进行分析。
/**
* -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
**/
public class HeapOOM {
static class OOMObject{
}
public static void main(String[] args) {
List<OOMObject> list =new LinkedList<OOMObject>();
while (true){
list.add(new OOMObject());
}
}
}
输出结果如下
这里虽然模拟出了OOM,但是从Error Message来看是GC overhead limit exceeded,并非预期的Java heap space,原因是这个是JDK6新添的错误类型。是发生在GC占用大量时间为释放很小空间的时候发生的,是一种保护机制。一般是因为堆太小,导致异常的原因:没有足够的内存。 Sun 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。JVM给出这样一个参数:-XX:-UseGCOverheadLimit 禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,我们加上这个参数再次运行一次。结果如下:
2、虚拟机栈和本地方法栈溢出
由于在 HotSpot虚拟机中并不区分虚拟机栈和本地方法栈,因此,对于 HotSpot来说,虽然-Xoss参数(设置本地方法栈大小)存在,但实际上是无效的,栈容量只由-Xss参数设定。关于虚拟机栈和本地方法栈,在Java虚拟机规范中描述了两种异常:
- 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 StackOverflowError异常。
- 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出 OutOfMemoryError异常。
这里把异常分成两种情况,看似更加严谨,但却存在着一些互相重叠的地方:当栈空间无法继续分配时,到底是内存太小,还是已使用的栈空间太大,其本质上只是对同一件事情的两种描述而已。
《Java虚拟机规范》明确允许Java虚拟机实现自行选择是否支持栈的动态扩展,而HotSpot虚拟机 的选择是不支持扩展,所以除非在创建线程申请内存时就因无法获得足够内存而出现 OutOfMemoryError异常,否则在线程运行时是不会因为扩展而导致内存溢出的,只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常。 为了验证这点,我们可以做两个实验,先将实验范围限制在单线程中操作,尝试下面两种行为是 否能让HotSpot虚拟机产生OutOfMemoryError异常:
/*** VM Args:-Xss128k **/
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;
}
}
}
这里没有如我所愿出现StackOverflowError,而是提示程序运行所需要的栈内存至少为160K,我们需改-Xss为160K重新运行。
public class JavaVMStackSOF {
private static int stackLength = 0;
public static void test() {
long unused1, unused2, unused3, unused4, unused5, unused6, unused7, unused8, unused9, unused10, unused11, unused12, unused13, unused14, unused15, unused16, unused17, unused18, unused19, 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, unused98, unused99, unused100;
stackLength ++;
test();
unused1 = unused2 = unused3 = unused4 = unused5 = unused6 = unused7 = unused8 = unused9 = unused10 = unused11 = unused12 = unused13 = unused14 = unused15 = unused16 = unused17 = unused18 = unused19 = 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 = unused98 = unused99 = unused100 = 0;
}
public static void main(String[] args) {
try {
test();
}catch (Error e){
System.out.println("stack length:" + stackLength);
throw e;
}
}
}
![](https://i-blog.csdnimg.cn/blog_migrate/5693fdf48b6d4487e68471699802a92c.png)
下面通过不断地建立线程的方式产生内存溢出异常。
public class StackOOM {
private void dontStop(){
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void stackLeak(){
while (true){
new Thread(()->dontStop()).start();
}
}
public static void main(String[] args) {
StackOOM oom=new StackOOM();
oom.stackLeak();
}
}
3、方法区和运行时常量池溢出
由于运行时常量池是方法区的一部分,因此这两个区域的溢出测试就放在一起进行。自JDK1.7开始逐步“去永久代”, 并在JDK 8中完全使用元空间来代替永久代的背景, 在此我们就以测试代码来观察一下,使用“永久代”还是“元空间”来实现方法区对程序的实际影响。
String.intern()是一个 Native方法,它的作用是:如果字符串常量池中已经包含一个等于此 String对象的字符串,则返回代表池中这个字符串的 String对象;否则,将此 String对象包含的字符串添加到常量池中,并且返回此 String对象的引用。在JDK1.6及之前的版本中,由于常量池分配在永久代内,我们可以通过- XX:PermSize和-XX:MaxPermSize限制方法区大小,从而间接限制其中常量池的容量。
/**
* -XX:PermSize=10M -XX:MaxPermSize=10
**/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
//使用List保持着常量池引用,避免Full GC回收常量池行为
List<String> list = new LinkedList<String>();
//10MB的PermSize在integer范围内足够产生OOM了
int i = 0;
while (true) {
list.add(String.valueOf(i++).intern());
}
}
}
运行结果:
从运行结果中可以看到,运行时常量池溢出,在 OutOfMemoryError后面跟随的提示信息是“ PermGen space”,说明运行时常量池属于方法区(HotSpot虚拟机中的永久代)的一部分。
而使用JDK1.7和JDK1.8运行这段程序就不会得到相同的结果, while循环将一直进行下去。并且JDK1.8还会提示“ignoring option PermSize=10M; support was removed in 8.0”如下,因为从Java8开始移除了永久代。
关于这个字符串常量池的实现问题,还可以引申出一个更有意思的影响,如下所示。
String str1=new StringBuilder("计算机").append("软件").toString();
System.out.println(str1==str1.intern());
String str2=new StringBuilder("ja").append("va").toString();
System.out.println(str2==str2.intern());
这段代码在JDK1.6中运行,会得到两个 false,而在JDK1.7中运行,会得到一个true和一个 false。产生差异的原因是:在JDK1.6中, intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用,而由 StringBuilder创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回 false。而JDK1.7(以及部分其他虚拟机,例如 JRockit)的 intern()实现不会再复制实例,只是在常量池中记录首次出现的实例引用,因此 intern()返回的引用和由 StringBuilder刨建的那个字符串实例是同一个。对st2比较返回 false是因为“java”这个字符串在执行 StringBuilder.toString()之前已经出现过,字符串常量池中已经有它的引用了,不符合“首次出现”的原则,而“计算机软件”这个字符串则是首次出现的,因此返回true。
方法区用于存放 Class的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。对于这些区域的测试,基本的思路是运行时产生大量的类去填满方法区,直到溢出。虽然直接使用 Java SE API也可以动态产生类(如反射时的 GeneratedConstructor Accessor和动态代理等),但在本次实验中操作起来比较麻烦。在下面代码中借助CGLib直接操作字节码运行时生成了大量的动态类。
/**
* -XX:PermSize=8M -XX:MaxPermSize=8M -Xmx8M
**/
public class JavaMethodAreaOOM {
public static void main(final String[] args) {
while (true){
Enhancer enhancer=new Enhancer();
enhancer.setSuperclass(JavaMethodAreaOOM.class);
enhancer.setUseCache(false);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o,args);
}
});
enhancer.create();
}
}
}
上面这段代码在JDK1.6中会报java.lang.OutOfMemoryError: PermGen space;而在JDK1.7中则报java.lang.OutOfMemoryError: Java heap space。在JDK1.8中移除了永久代,增加了Meta space因此不受堆内存限制,我们指定MetaSpaceSize 和 MaxMetaSpaceSize来限制MetaSpace大小,运行结果如下。
4、本机直接内存溢出
/*** VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M **/
public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_1MB);
}
}
}