浅析JVM堆溢出和栈溢出以及其产生可能原因的排查要点
一、JVM 堆溢出
在 jvm 运行 java 程序时,如果程序运行所需要的内存大于系统的堆最大内存(-Xmx),就会出现堆溢出问题。创建对象时如果没有可以分配的堆内存,JVM就会抛出OutOfMemoryError:java heap space异常。
// 执行该段代码需要大于10m内存空间
public class HeadOverflow {
public static void main(String[] args) {
List<Object> listObj = new ArrayList<Object>();
for(int i=0; i<10; i++){
Byte[] bytes = new Byte[1*1024*1024];
listObj.add(bytes);
}
System.out.println("添加success");
}
}
// 设置该程序的jvm参数信息
-Xms1m -Xmx10m -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
初始堆内存和最大可以堆内存 Gc详细日志信息
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid2464.hprof ...
Heap dump file created [16991068 bytes in 0.047 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.ghs.test.OOMTest.main(OOMTest.java:16)
在正式项目部署环境程序默认读取的是系统的内存,一般设置程序的堆初始内存(-Xms) == 堆最大可用内存(-Xmx)。
二、JVM 栈溢出
1、栈溢出介绍:栈空间不足时,需要分下面两种情况处理:
(1)线程请求的栈深度大于虚拟机允许的最大深度 - StackOverflowError
(2)虚拟机在扩展栈深度时,无法申请到足够的内存空间 - OutOfMemoryError
理解:每次方法调用都会有一个栈帧压入虚拟机栈,操作系统给JVM分配的内存是有限的,JVM分配给“虚拟机栈”的内存是有限的。如果方法调用过多,导致虚拟机栈满了就会溢出。这里栈深度就是指栈帧的数量。
2、案例
// 循环递归调用,一直达到jvm的最大深度
public class StackOverflow {
private static int count;
public static void count(){
try {
count++;
count();
} catch (Throwable e) {
System.out.println("最大深度:"+count);
e.printStackTrace();
}
}
public static void main(String[] args) {
count();
}
}
3、调整 jvm 栈大小:设置-Xss5m设置最大调用深度后调用
每个计算机都会有一个极限最大调用深度,避免递归在代码中无限循环。局部变量表内容越多,栈帧越大,栈深度越小。
这里有一篇文章讲的是各自溢出的问题,可以看看:《写代码实现堆溢出、栈溢出、永久代溢出、直接内存溢出 - https://blog.csdn.net/u011983531/article/details/63250882》
(1)栈内存溢出:程序所要求的栈深度过大。
(2)堆内存溢出: 分清内存泄露还是 内存容量不足。泄露则看对象如何被 GC Root 引用,不足则通过调大-Xms,-Xmx参数。
(3)永久代溢出:Class对象未被释放,Class对象占用信息过多,有过多的Class对象。
(4)直接内存溢出:系统哪些地方会使用直接内存。
三、内存溢出的原因是什么
1、内存溢出与内存泄漏:
内存溢出:申请内存空间,超出最大堆内存空间。
内存泄露:其实包含了内存溢出,堆内存空间被无用对象占用没有及时释放,导致占用内存,最终导致内存泄露。
2、内存溢出的可能原因排查
内存溢出是由于没被引用的对象(垃圾)过多造成JVM没有及时回收,造成的内存溢出。如果出现这种现象可进行代码排查:
(1)是否应用中的类中和引用变量过多使用了Static修饰,如 public staitc Students;
在类中的属性中使用 static 修饰的最好只用基本类型或字符串。如:public static int i = 0; public static String str;
(2)是否 应用 中使用了大量的递归或无限递归(递归中用到了大量的建新的对象)
(3)是否App中使用了大量循环或死循环(循环中用到了大量的新建的对象)
(4)检查 应用 中是否使用了向数据库查询所有记录的方法。即一次性全部查询的方法,如果数据量超过10万多条了,就可能会造成内存溢出。所以在查询时应采用“分页查询”。
(5)检查是否有数组,List,Map中存放的是对象的引用而不是对象,因为这些引用会让对应的对象不能被释放,会大量存储在内存中。
(6)检查是否使用了“非字面量字符串进行+”的操作。
因为String类的内容是不可变的,每次运行"+"就会产生新的对象,如果过多会造成新String对象过多,从而导致JVM没有及时回收而出现内存溢出。
// 如
String s1 = "My name";
String s2 = "is";
String s3 = "xuwei";
String str = s1 + s2 + s3 +.........; // 这是会容易造成内存溢出的
// 但是String str = "My name" + " is " + " xuwei" + " nice " + " to " + " meet you";
// 这种就不会造成内存溢出。因为这是”字面量字符串“,在运行"+"时就会在编译期间运行好。不会按照JVM来执行的。
在使用 String、StringBuffer、StringBuilder 时,如果是字面量字符串进行"+"时,应选用String性能更好;如果是String类进行"+"时,在不考虑线程安全时,应选用 StringBuilder 性能更好。
再比如下面这些错误的示例:
public class Test {
public void testHeap(){
for(;;){ //死循环一直创建对象,堆溢出
ArrayList list = new ArrayList (2000);
}
}
int num=1;
public void testStack(){ //无出口的递归调用,栈溢出
num++;
this.testStack();
}
public static void main(String[] args){
Test t = new Test ();
t.testHeap();
t.testStack();
}
}
3、栈溢出的原因
(1)是否有递归调用
(2)是否有大量循环或死循环
(3)全局变量是否过多
(4)数组、List、map数据是否过大
(5)使用DDMS工具进行查找大概出现栈溢出的位置