实战:OutOfMemoryError异常
Java堆溢出
Java堆用于存储对象实例,只要不断的创建对象并且保证GC Roots到对象之间有可达路径来避免垃圾回收,就可以触发Java堆的内存溢出异常
控制Java堆的扩展容量可以通过参数-Xms和-Xmx来设置,为更方便的获取到内存溢出时的内存快照数据可以使用参数-XX: HeapDumpOnOutOfMemoryError
- 代码示例
import java.util.ArrayList;
import java.util.List;
/**
* Java堆内存溢出异常测试
* {@link 《深入理解Java虚拟机》第三版 代码清单2-3}
* VM Args:-Xms2m -Xmx2m -XX: HeapDumpOnOutOfMemoryError
* 代码在JDK1.8下运行
*/
public class HeapOOM {
static class OOMObject {
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<>();
while (true) {
list.add(new OOMObject());
}
}
}
- 异常堆栈
- 关于GC overhead limit exceeded
运行结果和书中描述的异常堆栈不一致,异常堆栈描述中出现了GC overhead limit exceeded信息
Oracle官方给出了这个错误产生的原因和解决方法:
Exception in thread thread_name: java.lang.OutOfMemoryError: GC Overhead limit exceeded Cause: The detail message "GC overhead limit exceeded" indicates that the garbage collector is running all the time and Java program is making very slow progress. After a garbage collection, if the Java process is spending more than approximately 98% of its time doing garbage collection and if it is recovering less than 2% of the heap and has been doing so far the last 5 (compile time constant) consecutive garbage collections, then a java.lang.OutOfMemoryError is thrown. This exception is typically thrown because the amount of live data barely fits into the Java heap having little free space for new allocations. Action: Increase the heap size. The java.lang.OutOfMemoryError exception for GC Overhead limit exceeded can be turned off with the command line flag -XX:-UseGCOverheadLimit.
GC overhead limit exceeded,是JDK6新增的一个错误类型,根据官方的描述,这种错误类型描述了这样一种情形:Java虚拟机使用了98%的时间做GC,却只得到了2%的可用内存,以至于最终无内存可用,抛出了OutOfMemoryError
Oracle官方提供了-XX:-UseGCOverheadLimit参数禁用此类检查,使得异常堆栈中不再出现GC overhead limit exceeded信息;因此,为复现书中结果,可以选择加上此参数(注:这并不是一种解决方案,而只是关闭了一类错误类型的开关,根治还是要从代码检查和内存占用去实际分析)
- 对内存溢出时的快照
从快照数据中,可以看出造成此次内存溢出的原因:频繁创建且存活的对象
虚拟机栈和本地方法栈溢出
在Java虚拟机规范中,对虚拟机栈和本地方法栈描述了两种异常,同时允许Java虚拟机实现自行选择是否支持栈的动态扩展
- 当线程请求的栈深度大于虚拟机所允许的深度时,将抛出StackOverflowError异常
- 当虚拟机栈扩展时无法申请到足够内存时会抛出OutOfMemoryError异常
HotSpot虚拟机并不区分虚拟机栈和本地方法栈,同时,HotSpot虚拟机并不支持栈的动态扩展,所以除非在创建线程申请内存时就因为无法获得足够内存而出现OutOfMemoryError异常,否则在线程运行时是不会因为扩展而导致内存溢出,只会因为栈容量无法容纳新的栈帧而导致StackOverflowError异常
控制栈容量通过参数-Xss来设置
- 代码示例一:无法容纳新的栈帧而栈溢出
/**
* 虚拟机栈和本地方法栈测试
* {@link 《深入理解Java虚拟机》第三版 代码清单2-4}
* VM Args:-Xss128k
* 代码在JDK1.8下运行
*/
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;
}
}
}
- 异常堆栈
栈容量的配置,在不同版本的Java虚拟机和不同的操作系统,会有不同的栈容量最小值限制,此处堆栈信息表示最小配置640k,遂更改JVM参数为-Xss640k
- 代码示例二:无法容纳新的栈帧而栈溢出,同样的代码,增加了本地变量,异常出现时输出的堆栈深度相应缩小
/**
* 虚拟机栈和本地方法栈测试
* {@link 《深入理解Java虚拟机》第三版 代码清单2-5}
* VM Args:-Xss128k
* 代码在JDK1.8下运行
*/
public class JavaVMStackSOF {
private int stackLength = 1;
public void stackLeak() {
String s1 = "";
stackLength ;
stackLeak();
s1 = stackLength "Exception in thread thread_name: java.lang.OutOfMemoryError: GC Overhead limit exceeded Cause: The detail message "GC overhead limit exceeded" indicates that the garbage collector is running all the time and Java program is making very slow progress. After a garbage collection, if the Java process is spending more than approximately 98% of its time doing garbage collection and if it is recovering less than 2% of the heap and has been doing so far the last 5 (compile time constant) consecutive garbage collections, then a java.lang.OutOfMemoryError is thrown. This exception is typically thrown because the amount of live data barely fits into the Java heap having little free space for new allocations. n"
"Action: Increase the heap size. The java.lang.OutOfMemoryError exception for GC Overhead limit exceeded can be turned off with the command line flag -XX:-UseGCOverheadLimit."
stackLength "Exception in thread thread_name: java.lang.OutOfMemoryError: GC Overhead limit exceeded Cause: The detail message "GC overhead limit exceeded" indicates that the garbage collector is running all the time and Java program is making very slow progress. After a garbage collection, if the Java process is spending more than approximately 98% of its time doing garbage collection and if it is recovering less than 2% of the heap and has been doing so far the last 5 (compile time constant) consecutive garbage collections, then a java.lang.OutOfMemoryError is thrown. This exception is typically thrown because the amount of live data barely fits into the Java heap having little free space for new allocations. n"
"Action: Increase the heap size. The java.lang.OutOfMemoryError exception for GC Overhead limit exceeded can be turned off with the command line flag -XX:-UseGCOverheadLimit.";
}
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;
}
}
}
- 异常堆栈
- 代码示例三:创建线程申请内存时不足导致OutOfMemoryError
/**
* 虚拟机栈和本地方法栈测试
* {@link 《深入理解Java虚拟机》第三版 代码清单2-6}
* VM Args:-Xss640k
* 代码在JDK1.8下运行
*/
public class JavaVMStackOOM {
private void dontStop() {
while (true) {
}
}
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(() -> {
dontStop();
});
thread.start();
}
}
public static void main(String[] args) {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
- 异常堆栈
操作系统分配给每个进程的内存是有限制的,譬如32位Windows的单个进程最大内存限制为2GB。HotSpot虚拟机提供了参数可以控制Java堆和方法区这两部分的内存的最大值,那剩余的内存即为2GB减去最大堆容量,再减去最大方法区容量,由于程序计数器消耗内存很小,可以忽略掉,如果把直接内存和虚拟机进程本身耗费的内存也去掉的话,剩下的内存就由虚拟机和本地方法栈来分配了。因此为每个线程分配到的栈内存越大,可以建立的线程数量自然就越少,建立线程时就越容易把剩下的内存耗尽
如果是建立过多线程导致的内存溢出,在不能减少线程数量或者更换64位虚拟机的情况下,就只能通过减少最大堆和减少栈容量来换取更多的线程
方法区和运行时常量池溢出
方法区的主要职责是用于存放类型的相关信息,如类名、访问修饰符、运行时常量池、字段描述、方法描述等;对于这部分的测试,一个是利用String包中的intern()方法往运行时常量池中不断添加常量直到溢出,另一个是运行时产生大量的类来填满方法区直到溢出
JDK8之前,通过-XX:PermSize和-XX:MaxPermSize限制永久代的大小
- 代码示例一:运行时常量池导致内存溢出
import java.util.HashSet;
import java.util.Set;
/**
* 虚拟机栈和本地方法栈测试
* {@link 《深入理解Java虚拟机》第三版 代码清单2-7}
* VM Args:-XX:PermSize=2M -XX:MaxPermSize=2M
* 代码在JDK1.6下运行
*/
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
Set<String> set = new HashSet<>();
short i = 0;
while (true) {
set.add(String.valueOf(i ).intern());
}
}
}
- 代码示例二:操作字节码运行时生成大量动态类导致内存溢出
import java.util.HashSet;
import java.util.Set;
/**
* 虚拟机栈和本地方法栈测试
* {@link 《深入理解Java虚拟机》第三版 代码清单2-9}
* VM Args:-XX:PermSize=2M -XX:MaxPermSize=2M
* 代码在JDK1.7下运行
*/
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 OOMObejct {
}
}
元空间
JDK8以后,永久代已经由元空间替代,已经很难迫使虚拟机产生方法区的溢出异常了,不过,HotSpot还是提供了以下参数作为元空间的防御措施:
- -XX:MaxMetaspaceSize:设置元空间的最大值,默认是-1,不限制
- -XX:MetaspaceSize:指定元空间的初始空间大小,以字节为单位,达到该值就会触发垃圾收集进行类型卸载,同时收集器会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少空间,那么在不超过-XX:MaxMetaspaceSize的情况下,适当提高该值
- -XX:MinMetaspaceFreeRatio:在垃圾收集之后控制最小的元空间剩余容量的百分比,可减少因为元空间不足导致的垃圾收集频率。类似的还有-XX:MaxMetaspaceFreeRatio,用于控制最大的元空间剩余容量的百分比
本机直接内存溢出
直接内存的容量大小可通过-XX:MaxDirectMemorySize参数来指定,默认与Java堆最大值(-Xmx)一致
- 代码示例
import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
* 虚拟机栈和本地方法栈测试
* {@link 《深入理解Java虚拟机》第三版 代码清单2-9}
* VM Args:-Xmx2m -XX:MaxDirectMemorySize=1m
* 代码在JDK1.8下运行
*/
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);
}
}
}
- 异常堆栈