JVM内存溢出OOM实验
本文是在JDK1.7下面做的试验,通过模拟,我们可以直接点中这些场景的本质,从而在纷繁复杂的千万行代码中避免这样去 coding。导致 OOM 的情况有多种,包括 Java 或 Native Method Stack 的内存不足或者栈空间溢出(stack over Flow)、Heap 内存溢出(OOM)、Non-heap 内存溢出(OOM)、Direct Memory 溢出。
1.java 方法栈溢出
什么时候会让 Java Method Stack 栈溢出啊?栈的基本特点就是 FILO(First In Last Out),如果 in 的太多而 out 的太少,就好 overflow 了。而 Java Method Stack 的功能就是保存每一次函数调用时的“现场”,即为入栈,函数返回就对应着出栈,所以函数调用的深度越大,栈就变得越大,足够大的时候就会溢出。所以模拟 Java Method Stack 溢出,只要不断递归调用某一函数就可以。
package com.elong.ihotel.util; /** * 测试栈溢出的情况 * @author user * */ public class TestStackOverFlow implements Runnable { private int stackLength = 0; @Override public void run() { this.stackOverFlow(); } /** * 递归的调用 */ public void stackOverFlow() { ++stackLength; stackOverFlow(); } public static void main(String[] args) throws InterruptedException { TestStackOverFlow r = new TestStackOverFlow(); try { //创建一个线程 Thread t = new Thread(r); Thread.sleep(20000); System.out.println("线程执行开始------"+t.getName()); //线程启动 t.start(); Thread.sleep(10000); System.out.println("线程执行结束------"+t.getName()); } catch (Exception e) { throw e; }finally{ System.out.println("栈的长度"+r.stackLength); } } }
运行结果:
使用JAVA - VisualVM观察如下:
2.java heap space 溢出
堆是用来存储对象的,当然对象不一定都存在堆里(由于逃逸技术的发展)。那么堆如果溢出了,一定是不能被杀掉的对象太多了。模拟 Heap 内存溢出,只要不断创建对象并保持有引用存在即可。
package com.elong.ihotel.util; import java.util.ArrayList; import java.util.List; /** * 测试栈溢出的情况 * @author user * */ public class TestheapOOM implements Runnable { private static class HeapOomObject { } @Override public void run() { List<HeapOomObject> list = new ArrayList<HeapOomObject>(); while (true) { list.add(new HeapOomObject()); } } public static void main(String[] args) throws InterruptedException { TestheapOOM r = new TestheapOOM(); try { // 创建一个线程 Thread t = new Thread(r); Thread.sleep(20000); System.out.println("线程执行开始------" + t.getName()); // 线程启动 t.start(); Thread.sleep(10000); System.out.println("线程执行结束------" + t.getName()); } catch (Exception e) { throw e; } } }
运行结果:
使用jstat命令观察结果:结果一直在FGC,最终老年代O区内存使用情况达到98.08%,垃圾回收不了,最终导致OOM。
使用JAVA - VisualVM观察如下:堆内存大小爆增加,CPU使用率达到85%多。
3.PermGen溢出:
也就是 Non-heap,是用来存储 Object Class Data、常量、静态变量、JIT 编译后的代码等。如果该区域溢出,则说明某种数据创建的实在是太多了。模拟的话,可以不断创建新的 class,直到溢出为止。
package com.elong.ihotel.util; import java.lang.reflect.Method; import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; /** * 测试栈溢出的情况 * @author user * */ public class TestMethodAreaOomObjectTest implements Runnable { static class MethodAreaOomObject{ } @Override public void run() { while (true) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(MethodAreaOomObject.class); enhancer.setUseCache(false); enhancer.setCallback(new MethodInterceptor() { public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { return proxy.invoke(obj, args); } }); enhancer.create(); } } public static void main(String[] args) throws InterruptedException { TestMethodAreaOomObjectTest r = new TestMethodAreaOomObjectTest(); try { // 创建一个线程 Thread t = new Thread(r); Thread.sleep(20000); System.out.println("线程执行开始------" + t.getName()); // 线程启动 t.start(); Thread.sleep(10000); System.out.println("线程执行结束------" + t.getName()); } catch (Exception e) { throw e; } } }
运行结果如下:
使用jstat命令观察结果:结果看出P区内存使用情况,一直在爆增加,最终使用了100%,导致P区内存溢出。
使用JAVA - VisualVM观察如下:从图中可以看出,class的数量一直在增加,达到了10074,permGen的内存一直在增加,最终把所有内存使用完,导致permGen内存溢出。
4. Runtime Constant Pool (字符串常量池)内存溢出:
做这个实验目的是想验证下JDK6和JDK7对字符串常量存储的变化。首先说下,在 Jdk6 以及以前的版本中,常量池是放在Perm区的,Perm区和正常的java heap区域是完全分开的,例如Sting st1 = "abc";(1)和string st2=new String("abc");(2) ;(1)是直接在常量池中生成,也就是说在Perm申请了一块内存,而new 出来的String对象是放在 java heap区。st1 == st2 ?很显然拿一个 JAVA Heap 区域的对象地址和字符串常量池的对象地址进行比较肯定是不相同的。而在 jdk7 的版本中,字符串常量池已经从 Perm 区移到正常的 Java Heap 区域了。那我们验证下到底字符串常量池是否移动到了Java Heap区域了呢?我们使用JDK1.7来验证。使用下面程序。
运行时常量可以用 String 类的 intern 方法,不断地产生新的常量。intern会判断字符串在常量池里面是否有一样的,如果有的话,就不会重新产生,所以我们要每次产生不同的字符串常量。
package com.elong.ihotel.util; import java.util.ArrayList; import java.util.List; /** * 溢出的情况 * @author user * */ public class TestConstantMethodAreaOomObjectTest { public static void main(String[] args) { List<String> list = new ArrayList<String>(); int i = 0; while (true) { System.out.println(String.valueOf(i++).intern()); list.add(String.valueOf(i++).intern()); } } }
运行结果:此结果需要运行很长时间,在本地用了大约50分钟,才观察到了溢出的结果。
使用jstat命令观察结果:E区和O区使用率达到了100%,但是P区使用率不变。一直在FullGc