java 內存溢出 棧溢出的緣由與排查方法

  • 一、 內存溢出的緣由是什麼?

內存溢出是因爲沒被引用的對象(垃圾)過多形成JVM沒有及時回收,形成的內存溢出。若是出現這種現象可行代碼排查:java

一)是否應用中的類中和引用變量過多使用了Static修飾 如public staitc Student s;在類中的屬性中使用 static修飾的最好只用基本類型或字符串。如public static int i = 0; //public static String str;數據庫

二)是否 應用 中使用了大量的遞歸或無限遞歸(遞歸中用到了大量的建新的對象)編程

三)是否App中使用了大量循環或死循環(循環中用到了大量的新建的對象)數組

四)檢查 應用 中是否使用了向數據庫查詢全部記錄的方法。即一次性所有查詢的方法,若是數據量超過10萬多條了,就可能會形成內存溢出。因此在查詢時應採用「分頁查詢」。安全

五)檢查是否有數組,List,Map中存放的是對象的引用而不是對象,由於這些引用會讓對應的對象不能被釋放。會大量存儲在內存中。多線程

六)檢查是否使用了「非字面量字符串進行+」的操做。由於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性能更好。

 

  1. public class Test {  
  2.   
  3.     public void testHeap(){  
  4.         for(;;){  //死循環一直建立對象,堆溢出
  5.               ArrayList list = new ArrayList (2000);  
  6.           }  
  7.     }  
  8.     int num=1;  
  9.     public void testStack(){  //無出口的遞歸調用,棧溢出
  10.         num++;  
  11.         this.testStack();  
  12.      }  
  13.       
  14.     public static void main(String[] args){  
  15.         Test  t  = new Test ();  
  16.         t.testHeap();  
  17.         t.testStack();     
  18.     }  
  • 二、棧溢出的緣由

      一)、是否有遞歸調用

二)、是否有大量循環或死循環

三)、全局變量是否過多

四)、 數組、List、map數據是否過大

五)使用DDMS工具進行查找大概出現棧溢出的位置

後續持續更新 請看到的及時補充到評論區……

下面是摘自掘金中的一篇文章,在項目過程當中或多或少遇到過,因爲本人不想再一一作測試用例,就摘錄過來了,最後附帶地址連接 ,能夠方便你們去看原文(尊重原著)

JVM系列之實戰內存溢出異常

實戰內存溢出異常

你們好,相信大部分Javaer在code時常常會遇到本地代碼運行正常,但在生產環境偶爾會莫名其妙的報一些關於內存的異常,StackOverFlowError,OutOfMemoryError異常是最多見的。今天就基於上篇文章JVM系列之Java內存結構詳解講解的各個內存區域重點實戰分析下內存溢出的狀況。在此以前,我仍是想多餘累贅一些其餘關於對象的問題,具體內容以下:

文章結構

  1. 對象的建立過程
  2. 對象的內存佈局
  3. 對象的訪問定位
  4. 實戰內存異常

1 . 對象的建立過程

關於對象的建立,第一反應是new關鍵字,那麼本文就主要講解new關鍵字建立對象的過程。

Student stu =new Student("張三""18");

就拿上面這句代碼來講,虛擬機首先會去檢查Student這個類有沒有被加載,若是沒有,首先去加載這個類到方法區,而後根據加載的Class類對象建立stu實例對象,須要注意的是,stu對象所需的內存大小在Student類加載完成後即可徹底肯定。內存分配完成後,虛擬機須要將分配到的內存空間的實例數據部分初始化爲零值,這也就是爲何咱們在編寫Java代碼時建立一個變量不須要初始化。緊接着,虛擬機會對對象的對象頭進行必要的設置,如這個對象屬於哪一個類,如何找到類的元數據(Class對象),對象的鎖信息,GC分代年齡等。設置完對象頭信息後,調用類的構造函數。
其實講實話,虛擬機建立對象的過程遠不止這麼簡單,我這裏只是把大體的脈絡講解了一下,方便你們理解。

2 . 對象的內存佈局

剛剛提到的實例數據,對象頭,有些小夥伴也許有點陌生,這一小節就詳細講解一下對象的內存佈局,對象建立完成後大體能夠分爲如下幾個部分:

  • 對象頭
  • 實例數據
  • 對齊填充

對象頭: 對象頭中包含了對象運行時一些必要的信息,如GC分代信息,鎖信息,哈希碼,指向Class類元信息的指針等,其中對Javaer比較有用的是鎖信息與指向Class對象的指針,關於鎖信息,後期有機會講解併發編程JUC時再擴展,關於指向Class對象的指針其實很好理解。好比上面那個Student的例子,當咱們拿到stu對象時,調用Class stuClass=stu.getClass();的時候,其實就是根據這個指針去拿到了stu對象所屬的Student類在方法區存放的Class類對象。雖說的有點拗口,但這句話我反覆琢磨了好幾遍,應該是說清楚了。^_^

實例數據:實例數據部分是對象真正存儲的有效信息,就是程序代碼中所定義的各類類型的字段內容。

對齊填充:虛擬機規範要求對象大小必須是8字節的整數倍。對齊填充其實就是來補全對象大小的。

3 . 對象的訪問定位

談到對象的訪問,還拿上面學生的例子來講,當咱們拿到stu對象時,直接調用stu.getName();時,其實就完成了對對象的訪問。但這裏要累贅說一下的是,stu雖然一般被認爲是一個對象,其實準確來講是不許確的,stu只是一個變量,變量裏存儲的是指向對象的指針,(若是幹過C或者C++的小夥伴應該比較清楚指針這個概念),當咱們調用stu.getName()時,虛擬機會根據指針找到堆裏面的對象而後拿到實例數據name.須要注意的是,當咱們調用stu.getClass()時,虛擬機會首先根據stu指針定位到堆裏面的對象,而後根據對象頭裏面存儲的指向Class類元信息的指針再次到方法區拿到Class對象,進行了兩次指針尋找。具體講解圖以下:

 

4 .實戰內存異常

內存異常是咱們工做當中常常會遇到問題,但若是僅僅會經過加大內存參數來解決問題顯然是不夠的,應該經過必定的手段定位問題,究竟是由於參數問題,仍是程序問題(無限建立,內存泄露)。定位問題後才能採起合適的解決方案,而不是一內存溢出就查找相關參數加大。

概念

  • 內存泄露:代碼中的某個對象本應該被虛擬機回收,但由於擁有GCRoot引用而沒有被回收。關於GCRoot概念,下一篇文章講解。
  • 內存溢出: 虛擬機因爲堆中擁有太多不可回收對象沒有回收,致使沒法繼續建立新對象。

在分析問題以前先給你們講一講排查內存溢出問題的方法,內存溢出時JVM虛擬機會退出,那麼咱們怎麼知道JVM運行時的各類信息呢,Dump機制會幫助咱們,能夠經過加上VM參數-XX:+HeapDumpOnOutOfMemoryError讓虛擬機在出現內存溢出異常時生成dump文件,而後經過外部工具(做者使用的是VisualVM)來具體分析異常的緣由。

下面從如下幾個方面來配合代碼實戰演示內存溢出及如何定位:

  • Java堆內存異常
  • Java棧內存異常
  • 方法區內存異常

Java堆內存異常

/** VM Args: //這兩個參數保證了堆中的可分配內存固定爲20M -Xms20m -Xmx20m //文件生成的位置,做則生成在桌面的一個目錄 -XX:+HeapDumpOnOutOfMemoryError //文件生成的位置,做則生成在桌面的一個目錄 //文件生成的位置,做則生成在桌面的一個目錄 -XX:HeapDumpPath=/Users/zdy/Desktop/dump/ */
public class HeapOOM {
    //建立一個內部類用於建立對象使用
    static class OOMObject {
    }
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
        //無限建立對象,在堆中
        while (true) {
            list.add(new OOMObject());
        }
    }
}

Run起來代碼後爆出異常以下:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to /Users/zdy/Desktop/dump/java_pid1099.hprof ...

能夠看到生成了dump文件到指定目錄。而且爆出了OutOfMemoryError,還告訴了你是哪一片區域出的問題:heap space

打開VisualVM工具導入對應的heapDump文件(如何使用請讀者自行查閱相關資料),相應的說明見圖:

"類標籤"

"類標籤"


切換到"實例數"標籤頁
"實例數標籤"

"實例數標籤"

 

分析dump文件後,咱們能夠知道,OOMObject這個類建立了810326個實例。因此它能不溢出嗎?接下來就在代碼裏找這個類在哪new的。排查問題。(咱們的樣例代碼就不用排查了,While循環太兇猛了)

Java棧內存異常

老實說,在棧中出現異常(StackOverFlowError)的機率小到和去蘋果專賣店買手機,買回來後發現是Android系統的機率是同樣的。由於做者確實沒有在生產環境中遇到過,除了本身做死寫樣例代碼測試。先說一下異常出現的狀況,前面講到過,方法調用的過程就是方法幀進虛擬機棧和出虛擬機棧的過程,那麼有兩種狀況能夠致使StackOverFlowError,當一個方法幀(好比須要2M內存)進入到虛擬機棧(好比還剩下1M內存)的時候,就會報出StackOverFlow.這裏先說一個概念,棧深度:指目前虛擬機棧中沒有出棧的方法幀。虛擬機棧容量經過參數-Xss來控制,下面經過一段代碼,把棧容量人爲的調小一點,而後經過遞歸調用觸發異常。

/** * VM Args: //設置棧容量爲160K,默認1M -Xss160k */
public class JavaVMStackSOF {
    private int stackLength = 1;
    public void stackLeak() {
        stackLength++;
        //遞歸調用,觸發異常
        stackLeak();
    }

    public static void main(String[] args) throws Throwable {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length:" + oom.stackLength);
            throw e;
        }
    }
}

結果以下:
stack length:751
Exception in thread "main" java.lang.StackOverflowError

能夠看到,遞歸調用了751次,棧容量不夠用了。
默認的棧容量在正常的方法調用時,棧深度能夠達到1000-2000深度,因此,通常的遞歸是能夠承受的住的。若是你的代碼出現了StackOverflowError,首先檢查代碼,而不是改參數。

這裏順帶提一下,不少人在作多線程開發時,當建立不少線程時,容易出現OOM(OutOfMemoryError),這時能夠經過具體狀況,減小最大堆容量,或者棧容量來解決問題,這是爲何呢。請看下面的公式:

線程數*(最大棧容量)+最大堆值+其餘內存(忽略不計或者通常不改動)=機器最大內存

當線程數比較多時,且沒法經過業務上削減線程數,那麼再不換機器的狀況下,你只能把最大棧容量設置小一點,或者把最大堆值設置小一點。

方法區內存異常

寫到這裏時,做者原本想寫一個無限建立動態代理對象的例子來演示方法區溢出,避開談論JDK7與JDK8的內存區域變動的過渡,但細想想,仍是把這一塊從始致終的說清楚。在上一篇文章中JVM系列之Java內存結構詳解講到方法區時提到,JDK7環境下方法區包括了(運行時常量池),其實這麼說是不許確的。由於從JDK7開始,HotSpot團隊就想到開始去"永久代",你們首先明確一個概念,方法區和"永久代"(PermGen space)是兩個概念,方法區是JVM虛擬機規範,任何虛擬機實現(J9等)都不能少這個區間,而"永久代"只是HotSpot對方法區的一個實現。爲了把知識點列清楚,我仍是才用列表的形式:

  • JDK7以前(包括JDK7)擁有"永久代"(PermGen space),用來實現方法區。但在JDK7中已經逐漸在實現中把永久代中把不少東西移了出來,好比:符號引用(Symbols)轉移到了native heap,運行時常量池(interned strings)轉移到了java heap;類的靜態變量(class statics)轉移到了java heap.
    因此這就是爲何我說上一篇文章中說方法區中包含運行時常量池是不正確的,由於已經移動到了java heap;
  • 在JDK7以前(包括7)能夠經過-XX:PermSize -XX:MaxPermSize來控制永久代的大小.
  • JDK8正式去除"永久代",換成Metaspace(元空間)做爲JVM虛擬機規範中方法區的實現。
  • 元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。所以,默認狀況下,元空間的大小僅受本地內存限制,但仍能夠經過參數控制:-XX:MetaspaceSize與-XX:MaxMetaspaceSize來控制大小。

下面做者仍是經過一段代碼,來不停的建立Class對象,在JDK8中能夠看到metaSpace內存溢出:

/** 做者準備在JDK8下測試方法區,因此設置了Metaspace的大小爲固定的8M -XX:MetaspaceSize=8m -XX:MaxMetaspaceSize=8m */
public class JavaMethodAreaOOM {

    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });
            //無限建立動態代理,生成Class對象
            enhancer.create();
        }
    }

    static class OOMObject {

    }
}

在JDK8的環境下將報出異常:
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
這是由於在調用CGLib的建立代理時會生成動態代理類,即Class對象到Metaspace,因此While一下就出異常了。
提醒一下:雖然咱們平常叫"堆Dump",可是dump技術不只僅是對於"堆"區域纔有效,而是針對OOM的,也就是說無論什麼區域,凡是可以報出OOM錯誤的,均可以使用dump技術生成dump文件來分析。

在常常動態生成大量Class的應用中,須要特別注意類的回收情況,這類場景除了例子中的CGLib技術,常見的還有,大量JSP,反射,OSGI等。須要特別注意,當出現此類異常,應該知道是哪裏出了問題,而後看是調整參數,仍是在代碼層面優化。

附加-直接內存異常

直接內存異常很是少見,並且機制很特殊,由於直接內存不是直接向操做系統分配內存,並且經過計算獲得的內存不夠而手動拋出異常,因此當你發現你的dump文件很小,並且沒有明顯異常,只是告訴你OOM,你就能夠考慮下你代碼裏面是否是直接或者間接使用了NIO而致使直接內存溢出。

好了,"JVM系列之實戰內存溢出異常"到這裏就給你們介紹完了,Have a good day .歡迎留言指錯。

往期入口:

  1. JVM系列之Java內存結構詳解
相關文章
相關標籤/搜索