上一篇文章我们讲了JVM运行时数据区域与内存溢出异常,其中对于内存溢出异常这部分将的不够详细,这篇文章将着重讲解Java内存溢出异常的相关知识。

Java的内存溢出异常主要分为两类:分别是内存溢出和栈溢出。在以下几种情况,会抛出内存异常:Java堆溢出、虚拟机栈和本地方法栈溢出、方法区和运行时常量池溢出、以及本机直接内存溢出,下面讲一一介绍这几类异常。

Java堆溢出

在Java内存区域与内存溢出异常中讲过,Java堆主要是用来存储对象实例的。这部分的内存区域的大小可以通过-Xms参数和-Xmx参数进行设置,通常将-Xms和-Xmx的值设置为相同的值,以减少内存扩展或者收缩时的开销。

Java堆的空间是有限的,受到物理内存与虚拟机内存的双重限制(通常虚拟机内存的会设置成小于物理内存)。因此,如果对象实例的数量不断增加,而垃圾回收机制没有进行及时清理的时候,对象实例所占用的空间就会达到Java堆的空间最大值。此时,就会因为Java堆内存不足,导致无法为新的实例分配空间,从而抛出OutOfMemoryError异常。

通过设置-Xms20m -Xmx20m运行以下代码可以模拟这一情况:



/**
 * VM Args: -Xms20m -Xmx20m
 *
 * @author bdq
 */
public class HeapOOM {
    static class OOMObject {

    }

    public static void main(String[] args) {
        List<OOMObject> objects = new ArrayList<>();
        while (true) {
            objects.add(new OOMObject());
        }
    }
}



  

运行结果:



java.lang.OutOfMemoryError: Java heap space



  



这即是常见的OOM异常,针对这类异常,往往在打印异常信息的同时会进一步提示异常原因,如上图所示的”Java heap space”。当然,只靠这点信息不足以判断到底是内存容量设置小了,还是出现了内存泄漏(关于内存泄漏的知识将会在后面的文章中进行讲述)。因此我们还要辅以其他手段来进一步确定问题的根源,比如加上-XX:+HeapDumpOnOutOfMemoryError参数使得虚拟机在出现内存溢出异常时Dump出当前的内存堆转储快照,然后用相关的工具进行分析。这类知识,本篇文章暂不作过多的讲解,将会在后面的文章一一介绍。

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

为什么要把虚拟机栈和本地方法栈的溢出放在一起讨论呢,因为在HotSpot虚拟机中并不区分虚拟机栈和本地方法栈。对于HotSpot来说,虽然说-Xoss参数是用来设置本地方法栈大小,但实际上是无效的,栈的容量只由-Xss参数设定。

在Java虚拟机规范中对虚拟机栈和本地方法栈描述了两种异常:

  1. 如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
  2. 如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

这种分类其实并不是很明确,因为内存太小或者已使用的栈空间太大都会导致栈空间无法继续分配。

StackOverflowError的出现条件很简单,下面这段简单的代码就会出现栈溢出:



public class JavaVMStackSOF {
    private int stackLength = 1;

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

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



  

运行结果如下:

 



Stack length:18663
Exception in thread "main" java.lang.StackOverflowError
	at cn.bdqfork.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
	at cn.bdqfork.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
	at cn.bdqfork.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
	at cn.bdqfork.jvm.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
    ......



  

对上面的运行结果,不同的计算机Stack length的大小是不确定的,从输出的异常信息来看,是因为stackLeak方法递归调用层数过多导致的。在大多数情况下,栈深度在虚拟机默认参数下是够用的。

OutOfMemoryError异常比较难以出现,一般发生在多线程环境下。当创建一个线程时,虚拟机会分配一个私有的栈空间给相应的线程,这个空间的大小可以用-Xss参数来设置。通过不断的创建新的进程,可以产生内存溢出异常。

原因是这样的,当进程运行时,操作系统分配给进程的内存是有限的,Java堆和方法区这两部分占了大部分,忽略到程序计数器所占用的很小的一块内存,不计算虚拟机本身占用的内存,剩下的就由虚拟机栈和本地方法栈所占用。因此,创建的线程数量到达一定程度时,虚拟机栈和本地方法栈所占用的空间就会使得进程的内存空间不够用,从而抛出内存溢出异常。

这部分的测试代码如下:



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 javaVMStackOOM = new JavaVMStackOOM();
        javaVMStackOOM.stackLeakByThread();
    }
}



  

这段代码的运行有一定的风险,因为Java的线程并不是完全的用户级线程,有映射到操作系统的部分,所以可能会产生系统假死的现象,请谨慎运行。

运行结果如下:



Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread



  

由此可以看出,我们在进行多线程开发时,对于线程的数量要有一定的把握,线程池的复用是很有必要的。

受限于篇幅原因,剩余的知识点,将会在下一篇进行讲解。