本文通过几段代码模拟实际的内存溢出异常。
文中代码都是基于Oracle公司的HotSpot虚拟机运行的。
1. Java堆溢出
1.1 模拟场景
Java堆用于存储对象,只要不断的创建对象,并保证GC Roots到对象之间有可达路径来避免垃圾回收机制清除这些对象,
那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。
package com.lindaxuan.outofmemory; import java.util.ArrayList; import java.util.List; /** * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError * 将堆的最小值-Xms参数与最大值-Xmx参数设置为一样即可避免堆自动扩展。 * @author linxuan */ public class HeapOOM { static class OOMObject { } public static void main(String[] args) { List<OOMObject> list = new ArrayList<OOMObject>(); while (true) { list.add(new OOMObject()); } } } /* result: java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid9220.hprof ... Heap dump file created [27717826 bytes in 0.160 secs] 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 com.lindaxuan.outofmemory.HeapOOM.main(HeapOOM.java:19) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:606) at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144) */
1.2 用内存影响分析工具分析堆快照
2.虚拟机栈和本地方法栈溢出
HotSpot虚拟机中不区分虚拟机栈和本地方法栈。栈容量用-Xss参数设定。Java虚拟机规范中描述了两种异常:
- 如果线程请求的栈深度大于虚拟机锁允许的最大深度,将抛出StackOverflowError异常。
- 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
2.1 StackOverflowError异常
package com.lindaxuan.outofmemory; /** * VM Args:-Xss128k * Error: Could not create the Java Virtual Machine. Error: A fatal exception has occurred. Program will exit. The stack size specified is too small, Specify at least 160k VM Args:-Xss256k * @author linxuan */ 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; } } } /* result: stack length:1868 Exception in thread "main" java.lang.StackOverflowError at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:18) at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19) at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19) at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19) at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19) at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19) at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19) at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19) at com.lindaxuan.outofmemory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:19) ... */
当单个线程下,无论是由于栈帧太大还是虚拟机栈容量太小,当内存无法分配的时候,虚拟机抛出的都是StackOverflowError异常。
2.2 OutOfMemoryError异常
package com.lindaxuan.outofmemory; /** * VM Args:-Xss2M (这时候不妨设大些) * @author linxuan */ 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) throws Throwable { JavaVMStackOOM oom = new JavaVMStackOOM(); oom.stackLeakByThread(); } } /* my result: run too long */
3.方法区和运行时常量池溢出
3.1 运行时常量区溢出
下面这段代码需要jdk1.6模拟。
package com.lindaxuan.outofmemory; import java.util.ArrayList; import java.util.List; /** * VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M * @author zzm * could not download jdk1.6 for macos */ public class RuntimeConstantPoolOOM { public static void main(String[] args) { // 使用List保持着常量池引用,避免Full GC回收常量池行为 List<String> list = new ArrayList<String>(); // 10MB的PermSize在integer范围内足够产生OOM了 int i = 0; while (true) { list.add(String.valueOf(i++).intern()); } } } /* result: run too long */
String.intern()返回引用的测试
package com.lindaxuan.outofmemory; public class RuntimeConstantPoolOOM2 { public static void main(String[] args) { String str1 = new StringBuilder("中国").append("钓鱼岛").toString(); System.out.println(str1.intern() == str1); String str2 = new StringBuilder("ja").append("va").toString(); System.out.println(str2.intern() == str2); } } /* result: true false */
对于jdk1.6,intern()方法会把首次遇到的字符串实例复制到永久代中,返回的也是永久代中这个字符串实例的引用。
而StringBuilder创建的字符串实例在Java堆,所以必然不是同一个引用,将返回false。
而jdk1.7中的intern()实现不会复制实例,只是在常量池中首次出现的实例引用,因此intern()返回的引用和由StringBuild创建的那个字符串实例是同一个。
3.2 String创建对象和对应内存状态
再看另一段代码
public class StringConstantPool { public static void main(String[] args) { String str1 = new StringBuilder("中国").append("钓鱼岛").toString(); System.out.println(str1.intern() == str1); String str2 = new String("倚天不屠龙"); System.out.println(str2.intern() == str2); } } /* true false */
“中国钓鱼岛”和“倚天不屠龙”都在常量区中不存在,那么为什么输出结果一个是true,另一个是false呢?
这就涉及到创建String对象的原理。下面我们将代码和内存对应起来看一下。
String str1 = new StringBuilder("中国").append("钓鱼岛").toString(); ////String创建对象时,会把参数"中国"和“钓鱼岛”放到常量池中
内存状态1
System.out.println(str1.intern() == str1); // str1.intern()将str1的引用复制到常量池中
内存状态2
String str2 = new String("倚天不屠龙"); //String创建对象时,会把参数"倚天不屠龙"放到常量池中
内存状态3
System.out.println(str2.intern() == str2); // str2.intern()先去常量池中看有没有"倚天不屠龙",已经有了。
内存状态4 (和内存状态3一致)
3.3 运行时方法区溢出
下面一段代码借助CGLib使方法区出现内存溢出异常。
package com.lindaxuan.outofmemory; import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; import java.lang.reflect.Method; /** * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M * @author linxuan */ public class JavaMethodAreaOOM { public static void main(String[] args) { while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(OOMObject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { return proxy.invokeSuper(obj, args); } }); enhancer.create(); } } static class OOMObject { } } /* * result: Exception in thread "main" Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main" */
4.本机直接内存溢出
DirectMemory容量可通过-XX: MaxDirectMemorySize指定,如果不指定,则默认与Java堆最大值 (-Xmx指定)一样。
package com.lindaxuan.outofmemory; import sun.misc.Unsafe; import java.lang.reflect.Field; /** * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M * @author linxuan */ 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); } } }
以上是模拟各种类型的内存溢出异常。
注:
本文的代码基于于深入理解Java虚拟机 2.4 实战:OutOfMemoryError异常,有轻微调整。
虚拟机的参数根据机器性能不同可以灵活调整。
5. JVM相关wiki和工具
内存分析工具,memory analyzer下载