在Java虚拟机规范中,除了程序计数器外,虚拟机内存的其他几个运行时区域都可能会发生OutOfMemoryError异常。

在IDEA中添加JVM参数如下:

OutOfMemoryError异常的几种原因_Java         OutOfMemoryError异常的几种原因_Java_02



一、Java堆溢出

        Java堆主要是用来存储对象,系统中不断的创建对象,并且在GC Roots到对象之间有可达路径,使垃圾回收机制不会回收这些对象,那么在对象数量达到最大堆的容量限制后就会产生内存溢出异常。代码如下:

/**
* JVM参数:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=E:\test\heapdump.hprof
* -Xms:堆的最小内存
* -Xmx:堆的最大内存
* -XX:+HeapDumpOnOutOfMemoryError :出现内存溢出时Dump出当前内存堆转储快照
* -XX:HeapDumpPath= :快照的存放路径
*/
public class HeapOOM {

static class OOMObject{}

public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
//不停的创建OOMObject
while (true){
list.add(new OOMObject());
}
}

}

    运行结果:

OutOfMemoryError异常的几种原因_CGLib_03

    堆内存溢出在实际应用中还是很常见的,出现堆内存溢出时,异常信息“java.lang.OutOfMemoryError”后会进一步提示 “Java heap space”。出现这个异常可以通过分析工具分析,确认时内存泄漏还是内存溢出。如果是泄露的话,继续使用工具分析具体泄露位置;不存在泄露,看看是不是可以加大堆内存容量。


二、虚拟机栈和本地方法栈溢出

    HotSpot虚拟机不区分虚拟机栈和本地方法栈,所以设置本地方法栈的参数-Xoss并没有效果;栈的容量只由-Xss参数设定。关于栈,Java虚拟机规范描述了两种异常:①如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。②如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。我们这里仅测试StackOverflowError异常如下:

/**
* JVM参数:-Xss128k
* 默认栈容量深度为:19029
* 修改栈容量为128k深度为:1001
* 通过减小栈的容量,可见栈的深度也变小。
*/
public class JavaVMStackSOF {

private int stackLength = 1;

public void stackLeak() {
stackLength++;
stackLeak();
}

public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try{
oom.stackLeak();
}catch (Throwable e){
System.out.println("Stack Length :" + oom.stackLength);
throw e;
}
}
}

    运行结果:(不同机器测试结果数量可能不同)

默认情况下: OutOfMemoryError异常的几种原因_Hibernate_04                                  -Xss128k:  OutOfMemoryError异常的几种原因_CGLib_05

       虚拟机使用默认参数,栈深度在大多数情况下,完全够用了(包括递归)。


三、方法区(元数据区)溢出

        在JDK7之前方法区可以通过PermSize永久代大小和MaxPermSize最大永久代大小设置,如:-XX:PermSize=10m -XX:MaxPermSize=10m 。但是在JDK8中已经完全移除了永久代,PermSize和MaxPermSize参数也一并移除了。在移除了Perm区域之后,JDK8中使用MetaSpace来替代,这些空间都直接在堆上来进行分配。  在JDK8中,类的元数据存放在native堆中,这个空间被叫做:元数据区。JDK8中给元数据区添加了一些新的参数:

    ①-XX:MetaspaceSize=<NNN> :<NNN>是分配给类元数据区(以字节计)的初始大小(初始高水位),超过会导致垃圾收集器卸载类。这个数量是一个估计值。当第一次到达高水位的时候,下一个高水位是由垃圾收集器来管理的。

    ②-XX:MaxMetaspaceSize=<NNN> :<NNN>是分配给类元数据区的最大值(以字节计)。这个参数可以用来限制分配给类元数据区的大小。这个值也是个估计值。默认无上限。

    ③-XX:MinMetaspaceFreeRatio=<NNN>:<NNN>是一次GC以后,为了避免增加元数据区(高水位)的大小,空闲的类元数据区的容量的最小比例,不够就会导致垃圾回收。

    ④  -XX:MaxMetaspaceFreeRatio=<NNN>:<NNN>是一次GC以后,为了避免减少元数据区(高水位)的大小,空闲的类元数据区的容量的最大比例,超过就会导致垃圾回收。

    借助CGLib使元数据区出现内存溢出测试如下:

/**
* JVM参数:-XX:MetaspaceSize=5M -XX:MaxMetaspaceSize=5M (JDK8)
* -XX:PermSize=10m -XX:MaxPermSize=10m (JDK7)
* 需要引入cglib依赖
*/
public class JavaMethodAreaOOM {

public static void main(final 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[] objects, MethodProxy proxy) throws Throwable {
return proxy.invokeSuper(obj,args);
}
});
enhancer.create();
}
}

static class OOMObject{}

}

    运行结果:

OutOfMemoryError异常的几种原因_IntelliJ IDEA_06

        在很多主流框架中都是用到了CGLib,如Spring、Hibernate,需要增强的类越多,就需要越大的元数据区来保证动态生成的Class可以加载入内存。


四、本机直接内存溢出

        本机直接内存溢出(程序中直接或简介使用了NIO会导致),不像上面几种OutOfMemoryError会告诉我们溢出的位置,如下:

/**
* JVM参数:-Xmx20m -XX:MaxDirectMemorySize=10m
*/
public class DircetMemoryOOM {

private static final int _1MB = 1024*1024;

public static void main(String[] args) throws IllegalAccessException {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true){
unsafe.allocateMemory(_1MB);
}
}
}

    运行结果:

        OutOfMemoryError异常的几种原因_CGLib_07



源代码:https://gitee.com/itcaofanqi/CaoFanqiStudyRepository/tree/master/stujvm

参考:周志明《深入理解Java虚拟机》