Java内存溢出异常(上)
Java内存溢出异常主要分为两类:内存溢出和堆栈溢出。在下列情况下,将引发内存异常:Java堆溢出、虚拟机堆栈和本地方法栈溢出、方法区域和运行时常数池溢出以及本地直接内存溢出。以下各节将逐一描述这些类型的异常。
Java堆溢出
正如Java内存区域和内存溢出异常中所提到的,Java堆主要用于存储对象实例。这部分内存区域的大小可以由-xms参数和-xmx参数设置。通常将-xms和-xmx的值设置为相同的值,以减少内存扩展或收缩的开销。
Java堆在空间上受到限制,同时受到物理内存和虚拟机内存的限制(虚拟机内存通常设置为小于物理内存)。因此,如果对象实例数量增加,垃圾收集机制不及时清理,对象实例占用的空间将达到Java堆的最大值。此时,由于Java堆内存不足,引发了OutOfMeMyLogError异常,这使得不可能为新实例分配空间。
通过设置-Xms20m -Xmx20m运行以下代码可以模拟这一情况:
运行结果:
java.lang.OutOfMemoryError: Java heap space
这是一个常见的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();
}
}
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
由此可以看出,在开发多线程时,我们应该对线程的数量有一定的保证,线程池的重用是非常必要的。
未完待续~