OutOfMemoryError异常

OutOfMemoryError异常

前言

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。

Java虚拟机运行时数据区域有下面两种,
线程共享:方法区(Method Area),堆(Heap)。
线程隔离:程序计数器(Program Counter Register),虚拟机栈(VM Stack),本地方法栈(Native Method Stack)。
除程序计数器外,每一个都有可能发生OutOfMemoryError,这里就是跟着《深入理解Java虚拟机 JVM高级特性与最佳实践》来通过代码触发这些异常,验证各运行时区域存储的内容,并学习根据异常信息快速判断是哪个区域的内存溢出,知道什么样的代码可能会导致这些区域内存溢出,以及该如何处理。

注:本篇内容基本都有来自于周志明的著作《深入理解Java虚拟机 JVM高级特性与最佳实践》。

准备

了解Java虚拟机的内存相关基础知识;
了解Heap Dump文件的获取并安装Eclipse Memory Analyzer插件1

实战

1 Java堆溢出

Java堆用于存储实例对象,只要不断创建对象,并保证GC Root到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。
所用虚拟机参数:-Xms、-Xmx、-XX:+HeapDumpOnOutOfMemoryError。

import java.util.ArrayList;
import java.util.List;

/**
 * VM Args: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
 * @author rhzhou
 */
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.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid11269.hprof ...
Heap dump file created [27541830 bytes in 0.083 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:3210)
	at java.util.Arrays.copyOf(Arrays.java:3181)
	at java.util.ArrayList.grow(ArrayList.java:265)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
	at java.util.ArrayList.add(ArrayList.java:462)
	at part2.HeapOOM.main(HeapOOM.java:19)

通过内存堆转储快照可以得到发生异常时的内存中的各种信息,判断是发生了内存泄露(Memory Leak)还是内存溢出(Memory Overflow)。
内存堆转储快照
如果是内存泄露,可进一步通过工具查看泄露对象到GC Root的引用链,找到泄露对象是通过怎样的路径与GC Root相关联并导致垃圾收集器无法自动回收它们的。
如果不存在泄露,则内存中的对象确实都是必须活着的,那就需要检查虚拟机的堆参数(-Xms与-Xmx),与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。

2 虚拟机栈和本地方法栈溢出

2.1 StackOverflowError

通过无限递归函数使得栈帧数量过多,所占容量超过所指定的线程栈内存,导致发生StackOverflowError异常。
虚拟机参数-Xss可以指定栈内存容量。

/**
 * VM Args: -Xss228k
 * @author rhzhou
 *
 */
public class JavaVMStackSOF {
	private int stackLength = 1;
	
	public void stackLeak() {
		stackLength++;
		stackLeak();
	}
	
	public static void main(String[] args) {
		JavaVMStackSOF oom = new JavaVMStackSOF();
		try{
			oom.stackLeak();
		} catch (Throwable e) {
			System.out.println("Stack length:" + oom.stackLength);
			throw e;
		}
	}
}

运行结果:

Stack length:1518
Exception in thread "main" 
java.lang.StackOverflowError
	at part2.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
	at part2.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
	at part2.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
	at part2.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
........................

虚拟机栈内存总使用量由栈帧数量、栈帧大小和线程数决定。所以如果是建立过多线程导致的内存溢出,在不能减少线程数的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程。

2.2 OutOfMemoryError

设定-Xss为2M,通过无限增加线程数来增加虚拟机栈所使用的内存。

/**
 * VM Args: -Xss2M
 * @author rhzhou
 *
 */
public class JavaVMStackOOM {
	private void dontStop() {
		while(true) {
			
		}
	}
	
	public void stackLeakByThread() {
		while(true) {
			Thread thread = new Thread(new Runnable() {
				@Override
				public void run() {
					dontStop();
				}
			});
			thread.start();
		}
	}
	
	public static void main(String[] args) {
		JavaVMStackOOM oom = new JavaVMStackOOM();
		oom.stackLeakByThread();
	}
}

由于程序执行后centos7系统明显变慢,1分钟内平均负载达到1000,程序所占虚拟内存逐渐达到10.8g,所以我就把程序杀掉了。理论上该程序应该是会把所有内存用光的。
在这里插入图片描述

3 方法区和运行时常量池溢出

3.1 运行时常量池溢出

如果在JDK1.6及以下版本中执行下面程序,会报出OutOfMemoryError:PermGen space异常。

/**
 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
 * @author rhzhou
 *
 */
public class RuntimeConstantPoolOOM {
	public static void main(String[] args) {
		List<String> list = new ArrayList<>();
		int i = 0;
		while(true) {
			list.add(String.valueOf(i++).intern());
		}
	}
}

而JDK1.7开始逐步“去永久代”,在JDK1.8中执行则会报下面的警告后正常执行:

Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=10M; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=10M; support was removed in 8.0
3.2 方法区溢出

同样的原因,通过CGLIB大量生成动态类的办法也没能触发OutOfMemoryError异常,可见追逐新版本的重要性。

import java.lang.reflect.Method;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

/**
 * VM Args: -XX:PermSize=10M -XX:MaxPermSize=10M
 * @author rhzhou
 *
 */
public class JavaMethodAreaOOM {
	static class OOMObject{	}
	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();
		}
	}
}

4 本机直接内存溢出

import java.lang.reflect.Field;

/**
 * VM Args: -Xmx20M -XX:MaxDirectMemorySize=10M
 * @author rhzhou
 *
 */
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);
		}
	}
}

由于Unsafe类报错,且我还没搞懂它,所以。。。这里先跳过,以后我弄明白了,再来修改。

参考链接


  1. https://my.oschina.net/biezhi/blog/286223 ↩︎

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值