Java JVM 3-内存区域OOM异常以及和*Error的区别
上一节我们讲过,内存区域除了线程私有的程序计数器区之外,都存在OOM。
1. Java虚拟机栈和本地方法栈溢出
关于虚拟机栈会产生的两种异常:
如果线程请求的栈深度大于虚拟机所允许的最大深度,会抛出*内存溢出异常。
如果虚拟机在拓展栈时无法申请到足够的内存空间,则会抛出OOM泄露异常。
栈上主要发生的是*Error,所以我们对其进行分析。
出现*Error异常时有错误堆栈信息可以阅读,可以直观的找到问题所在。如果使用虚拟机默认运行参数,栈的深度对于正常的方法调用(包括递归),完全够用。我们可以通过修改运行参数 -Xss 将栈的容量变小来观察异常的出现。
观察单线程*Error的例子。
/**
* 修改运行参数为:-Xss256K
* 方便观察效果
*/
public class Test {
private int stackLen = 1;
// 没有出口的递归
public void leak() {
stackLen++;
leak();
}
public static void main(String[] args) {
Test test = new Test();
try {
test.leak();
} catch (Throwable e) {
throw e;
}
}
}
运行结果:
观察多线程下内存溢出:
public class Test {
// 死循环
private void donstop() {
while (true) {
}
}
// 循环开启线程
public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
donstop();
}
});
thread.start();
}
}
public static void main(String[] args) {
Test test = new Test();
test.stackLeakByThread();
}
}
运行结果:
哪位同学要是能坚持到蓝屏之前杀掉这个进程,请留言。
如果是因为多线程导致的内存溢出问题,在不能减少线程数的情况下,只能减少最大堆和减少栈容量的方式来换取更多线程。
2. java堆溢出
Java堆用于存储对象实例,所以只要不断的创建对象,并且保证GC Roots到对象之间有可达路径来避免GC清除这些对象,那么在对象数量达到最大堆容量后就会产生内存泄露异常(OOM)。
上节中已经讲到了,可以通过设置JVM运行参数-Xms:设置堆的最小值、-Xmx:设置堆最大值来使程序的最大堆容量等于最小堆容量,这样java堆就不能扩容了。下面我们来看一个Java堆OOM的测试。
观察Java Heap OOM:
import java.util.ArrayList;
import java.util.List;
/**
* -Xmx20m 堆最大容量
* -Xms20m 堆最小容量,最大最小相同,使堆不能自动扩容
* -XX:+HeapDumpOnOutOfMemoryError 当发生了OOM,会将异常信息进行dump并进行简单异常信息的打印
*/
public class Test {
static class OOM {
}
public static void main(String[] args) {
List<OOM> list = new ArrayList<>();
// 死循环创建对象实例,将堆空间耗尽
while (true) {
list.add(new OOM());
}
}
}
运行结果:
Java堆内存的OOM异常是实际应用中最常见的内存泄露情况。
当出现Java堆内存泄露时,异常堆栈信息”java.lang.OutOfMemoryError”会进一步提示”Java heap space”。
当出现”Java heap space”则很明确的告知我们,OOM发生在堆上。此时要对Dump出来的文件进行分析,分析OOM的产生到底是出现了内存泄漏(MemoryLeak)还是内存溢出(Memory Overflow)。
3. 内存泄露与内存溢出的区别
内存泄漏 : 泄漏对象无法被GC。(*Error为典型的内存泄露)
内存溢出 : 内存对象确实还应该存活。此时要根据JVM堆参数与物理内存相比较检查是否还应该把JVM堆内存调大;或者检查对象的生命周期是否过长。(OOM为典型的内存溢出)
发生了异常之后,如果将对的容量扩充一倍,还没有解决问题,则该异常为内存泄漏,如果解决了,为内存溢出。