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类报错,且我还没搞懂它,所以。。。这里先跳过,以后我弄明白了,再来修改。