这是一个常见的OOM异常。对于这种异常,在打印异常信息时通常会引发异常的原因,如图中所示的“Java堆空间”。当然,仅此信息还不足以确定内存容量设置是否很小或是否存在内存泄漏(有关内存泄漏的知识将在后面的文章中介绍)。因此,我们需要通过添加-xx:+heapDumpOnAutofMemoryError参数等其他方式进一步查明问题的根本原因,使dump在虚拟机发生内存溢出异常时,取当前内存堆转储快照,并用相关工具进行分析。这类知识在本文中不会解释得太多,但将在下面的文章中逐一介绍。
虚拟机栈和本地方法栈溢出
为什么要一起讨论虚拟机堆栈和本地方法堆栈的溢出,因为热点虚拟机中的虚拟机堆栈和本地方法堆栈没有区别。对于Hotspot,虽然-xoss参数用于设置本地方法堆栈的大小,但它实际上是无效的,堆栈的容量仅由-xss参数设置。
在Java虚拟机规范中,针对虚拟机栈和本地方法栈描述了两种异常:
-
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
-
如果虚拟机在扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。
这种分类不是很清楚,因为太少的内存或太多的堆栈空间会导致堆栈空间无法继续分配。
堆栈溢出错误是一个简单的条件。堆栈溢出错误是以下简单代码中的堆栈溢出:堆栈溢出错误、堆栈溢出错误、堆栈溢出错误、堆栈溢出错误、堆栈溢出错误、堆栈溢出错误、堆栈溢出错误
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)
…
对于上述结果,不同计算机的堆栈长度大小是不确定的。从输出异常信息来看,这是因为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();
}
}