浅析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工具进行查找大概出现栈溢出的位置

posted @ 2021-09-14 17:37  古兰精  阅读(3165)  评论(0编辑  收藏  举报