在Java内存区域中介绍了JVM的内存区域,其中除了程序计数器外,虚拟机内存的其他几个运行时区域都有发生OutOfMemoryError异常的可能。
一、栈溢出
每个Java方法在被调用的时候,都会创建一个栈帧并入栈,那么这里我们直接无限调用递归方法,即可让虚拟机栈溢出。
public class StackSOF {
private int stackLength = 1;
public void stackLeak(){
stackLength++;
stackLeak();
}
public static void main(String[] args) {
StackSOF stackSOF = new StackSOF();
try {
stackSOF.stackLeak();
} catch (Throwable e) {
System.out.println(stackSOF.stackLength);
e.printStackTrace();
}
}
}
上述截图中控制台中打印了很多次异常,这是因为我们一共调用了61702次方法,每个方法都会抛出一个异常,栈中存储的栈帧数量也是受到栈帧大小的影响的,如我们直接在方法中随机加入一些参数,我们再看看栈的深度。
public class StackSOF {
private int stackLength = 1;
public void stackLeak(String arg1, String arg2) {
stackLength++;
stackLeak(arg1, arg2);
}
public static void main(String[] args) {
StackSOF stackSOF = new StackSOF();
try {
stackSOF.stackLeak("123456789123456789", "abcdefgabcdefgabcdefgabcdefg");
} catch (Throwable e) {
System.out.println(stackSOF.stackLength);
e.printStackTrace();
}
}
}
注意: 上述结果中显示的都是java.lang.StackOverflowError
,其实在栈区也是可能发生java.lang.OutOfMemoryError
异常的,因为JVM只限制单个虚拟机栈的大小,栈区的空间是没有办法去限制的,因为在运行过程中会有线程不断的运行,所以没办法限制。这里只需不断建立线程,JVM申请栈内存,待机器没有足够的内存,栈区就会发生java.lang.OutOfMemoryError
异常。
二、堆溢出
堆空间一般是程序启动时就申请了,那么只需限制其大小,然后再申请超出最大堆内存空间即可,如下:
public class HeapOOM {
public static void main(String[] args) {
String[] arr = new String[6*1024*1024]; // 6m
}
}
在运行上述代码之前,我们一定要记得先设置虚拟机的启动参数,限制其堆空间大小为5m,如下
另外堆是JVM上最大的内存区域,我们申请的几乎所有的对象,都是在这里存储的,如果我们不断地创建对象,并且保证GC Roots到对象的之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后也会产生内存溢出的异常。
public class HeapOOM {
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
while (true) {
list.add(new Object());
}
}
}
在运行上述代码之前,可以添加虚拟机的参数-XX:+PrintGC
或者-XX:+PrintGCDetails
来打印GC的日志,这里我们直接向GC日志打印至工作台,也可以通过参数-Xloggc:filename
将其打印至我们指定目录的文件之中
我们会发现在发生了很多次的GC回收之后,会抛出java.lang.OutOfMemoryError: GC overhead limit exceeded
异常,对于这个异常,官方的解释是:
超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。
JVM给出这样一个参数:-XX:-UseGCOverheadLimit
禁用这个检查,其实这个参数解决不了内存问题,只是把错误的信息延后,替换成java.lang.OutOfMemoryError: Java heap space
三、运行时常量池溢出
JVM内存区域中的运行时常量池,在不同jdk版本之中都是不同的,在JDK1.6之前,我们的运行时常量池是包含在方法区之中的;在JDK1.7之后,我们的运行时常量池从方法区中移动到了Java堆之中。
所以我们的不同的JDK版本志宏运行时常量其溢出,可能会出现不同的结果,如在JDK1.6及其之前,运行时常量池溢出会导致方法区内存溢出,而在jdk1.7及其之后,运行时常量池溢出会导致堆的内存溢出。
这里我们可以使用String.intern()
方法,它的作用:如果字符串常量池中已经包含一个等于此String对象的字符串,则放回代表池中的这个字符串的String对象;否则,将此String对象包含的字符串添加到常量池中,并且返回此String对象的引用。
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.8版本,所以再运行前还需要限定一下堆的大小
四、方法区溢出
方法区在JDK的不同版本之中有不同的定义及参数设值,如在JDK1.6之前,我们的方法区中还包含了运行时常量池,这里我们就可以直接向运行时常量池中添加大量的数据,使其溢出,从而我们的方法区也会溢出。
由于我们使用的JDK1.8,所以上述我们演示运行时常量池溢出导致的是Java堆的溢出。既然我们不能借助运行时常量池溢出导致方法区内存溢出,那我们还可以使用CGLIB来无限的代理生成增强类,使其方法区溢出。
public class MethodAreaOOM {
public static void main(String[] args) {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Target.class);
enhancer.setCallback(new CglibProxy());
enhancer.setUseCache(false);
enhancer.create();
}
}
static class Target{
}
static class CglibProxy implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o,objects);
}
}
}
使用Cglib需要引入相应的jar包,依赖如下,也可以下载相应的 .jar 文件导入项目中
<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.5</version>
</dependency>
另外在JDK8之后,方法区大小就只受本机总内存的限制,测试前需要先设定一下方法区的大小
五、直接内存溢出
直接内存一般在网络通信NIO中使用较多,在我们的NIO中为我们提供了可以直接分配直接内存的方法,如下
import java.nio.ByteBuffer;
public class DirectMemoryOOM {
public static void main(String[] args) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(12*1024*1024);
}
}
六、Java异常体系
最后补充一点,上述在测试栈溢出中使用了try-catch,在catch代码块中进行了打印处理,注意我们选择catch (Throwable),而不是catch (Exception),因为我们内存溢出抛出的是Error了,而不是Exception
这里我们简单了解一下Java的异常体系:
- Throwable: Java中所有异常和错误类的父类。只有这个类的实例(或者子类的实例)可以被虚拟机抛出或者被java的throw关键字抛出。同样,只有其或其子类可以出现在catch子句里面。
- Error: Throwable的子类,表示严重的问题发生了,而且这种错误是不可恢复的。
- Exception: Throwable的子类,应用程序应该要捕获其或其子类(RuntimeException例外),称为checked exception。比如:IOException, NoSuchMethodException…
- RuntimeException: Exception的子类,运行时异常,程序可以不捕获,称为unchecked exception。比如:NullPointException。