1. 程式人生 > >OutOfMemoryError異常以及各區記憶體溢位

OutOfMemoryError異常以及各區記憶體溢位

   

    在java虛擬機器的規範描述中,除了程式計數器外虛擬機器記憶體的其他幾個執行時區域都會發生OutOfMemoryError異常的可能。在Java語言裡,可作為GC Roots物件的包括如下幾種:

    a.虛擬機器棧(棧楨中的本地變量表)中的引用的物件

    b.方法區中的類靜態屬性引用的物件

    c.方法區中的常量引用的物件

    d.本地方法棧中JNI的引用的物件

 

java堆溢位

        該區用於儲存物件的例項,只要不斷的建立物件並且保證GC Roots到物件之間有可達路徑來避免垃圾回收清除這些物件,在物件數量達到最大堆的容量限制後就會產生記憶體溢位,堆中的OOM異常是實際中最常見的記憶體溢位異常,當出現時不僅會提示OOM的Error還會進一步給出Java heap space。對於這個區域的異常關鍵在於確認記憶體中的物件是否是必要的,要分清是記憶體洩漏還是記憶體溢位

        1、記憶體洩漏memory leak:是指程式在申請記憶體後,無法釋放已申請的記憶體空間,一次記憶體洩漏似乎不會有大的影響,但記憶體洩漏堆積後的後果就是記憶體溢位。向系統申請分配記憶體進行使用(new),可是使用完了以後卻不歸還(delete),或者申請到的那塊記憶體你自己也不能再訪問,而系統也不能再次將它分配給需要的程式。就相當於你租了個帶鑰匙的櫃子,你存完東西之後把櫃子鎖上之後,把鑰匙丟了或者沒有將鑰匙還回去,那麼結果就是這個櫃子將無法供給任何人使用,也無法被垃圾回收器回收,因為找不到他的任何資訊。

        2、記憶體溢位 out of memory:指程式申請記憶體時,沒有足夠的記憶體供申請者使用,或者說,給了你一塊儲存int型別資料的儲存空間,但是你卻儲存long型別的資料,那麼結果就是記憶體不夠用,此時就會報錯OOM,即所謂的記憶體溢位。 一個盤子用盡各種方法只能裝4個果子,你裝了5個,結果掉倒地上不能吃了。這就是溢位。比方說棧,棧滿時再做進棧必定產生空間溢位,叫上溢,棧空時再做退棧也產生空間溢位,稱為下溢。就是分配的記憶體不足以放下資料項序列,稱為記憶體溢位。

        3、二者的關係:記憶體洩漏的堆積最終會導致記憶體溢位,記憶體溢位就是你要的記憶體空間超過了系統實際分配給你的空間,此時系統相當於沒法滿足你的需求,就會報記憶體溢位的錯誤。

        4、記憶體洩漏的分類(按發生方式來分類):

              1.常發性記憶體洩漏:發生記憶體洩漏的程式碼會被多次執行到,每次被執行的時候都會導致一塊記憶體洩漏。 

             2.偶發性記憶體洩漏:發生記憶體洩漏的程式碼只有在某些特定環境或操作過程下才會發生。常發性和偶發性是相對的。對於特定的環境,偶發性的也許就變成了常發性的。所以測試環境和測試方法對檢測記憶體洩漏至關重要。 

             3.一次性記憶體洩漏:發生記憶體洩漏的程式碼只會被執行一次,或者由於演算法上的缺陷,導致總會有一塊僅且一塊記憶體發生洩漏。比如,在類的建構函式中分配記憶體,在解構函式中卻沒有釋放該記憶體,所以記憶體洩漏只會發生一次。 

             4.隱式記憶體洩漏:程式在執行過程中不停的分配記憶體,但是直到結束的時候才釋放記憶體。嚴格的說這裡並沒有發生記憶體洩漏,因為最終程式釋放了所有申請的記憶體。但是對於一個伺服器程式,需要執行幾天,幾周甚至幾個月,不及時釋放記憶體也可能導致最終耗盡系統的所有記憶體。所以,我們稱這類記憶體洩漏為隱式記憶體洩漏。 

    

    什麼情況下會發生記憶體洩露:

     1、靜態集合類引起記憶體洩漏:像HashMap、Vector等的使用最容易出現記憶體洩露,這些靜態變數的生命週期和應用程式一致,他們所引用的所有的物件Object也不能被釋放,因為他們也將一直被Vector等引用著。

    2、當集合裡面的物件屬性被修改後,再呼叫remove()方法時不起作用。

    3、監聽器:在釋放物件的時候卻沒有去刪除這些監聽器,增加了記憶體洩漏的機會。

    4、各種連線:比如資料庫連線(dataSourse.getConnection()),網路連線(socket)和io連線,除非其顯式的呼叫了其close()方法將其連線關閉,否則是不會自動被GC 回收的。

    5、內部類和外部模組的引用:內部類的引用是比較容易遺忘的一種,而且一旦沒釋放可能導致一系列的後繼類物件沒有釋放。此外程式設計師還要小心外部模組不經意的引用,例如程式設計師A 負責A 模組,呼叫了B 模組的一個方法如: public void registerMsg(Object b); 這種呼叫就要非常小心了,傳入了一個物件,很可能模組B就保持了對該物件的引用,這時候就需要注意模組B 是否提供相應的操作去除引用。

    (1)非靜態內部類(成員內部類)

    (2)靜態內部類(巢狀內部類)

    (3)區域性內部類(定義在方法內或者作用域內的類,好似區域性變數,所以不能有訪問控制符和static等修飾)

    (4)匿名內部類(沒有名字,僅使用一次new個物件即扔掉類的定義)。匿名內部類的型別可以是如下幾種方式:介面匿名內部類、抽象類匿名內部類、類匿名內部類

    優先使用靜態內部類而不是非靜態的,因為非靜態內部類持有外部類引用可能導致垃圾回收失敗。如果你的靜態內部類需要宿主Activity的引用來執行某些東西,你要將這個引用封裝在一個WeakReference中,避免意外導致Activity洩露,被弱引用關聯的物件只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前記憶體是否足夠,都會回收 只被弱引用關聯的物件,只被說明這個物件本身已經沒有用處了。

    6、單例模式:不正確使用單例模式是引起記憶體洩漏的一個常見問題,單例物件在初始化後將在JVM的整個生命週期中存在(以靜態變數的方式),如果單例物件持有外部的引用,那麼這個物件將不能被JVM正常回收,導致記憶體洩漏。

    7、handler引起的記憶體洩漏

        一些處理上的思路:通過工具檢視洩露物件到GC Roots的引用鏈,找到洩露物件是通過怎樣的路徑與GC Roots產生關聯並導致垃圾收集器無法自動回收,這樣就可以確定出洩露程式碼的位置。如果不是洩露就是記憶體中的物件還必須存活,那就應該調整虛擬機器的堆引數,與機器實體記憶體對比看是否可以調大,從程式碼上檢查是否存在物件生命期過長、持有狀態時間過長,來減少記憶體的消耗。

第一步,修改JVM啟動引數,直接增加記憶體。(-Xms,-Xmx引數一定不要忘記加。)

第二步,檢查錯誤日誌,檢視“OutOfMemory”錯誤前是否有其 它異常或錯誤。

第三步,對程式碼進行走查和分析,找出可能發生記憶體溢位的位置。

 

虛擬機器棧和本地方法棧溢位

對於這兩塊空間java虛擬機器規範描述了兩種異常:

         1.如果執行緒請求的棧深度大於虛擬機器所允許的最大深度將丟擲StackOverflowError異常

        2.如果虛擬機器在擴充套件棧時無法申請到足夠的記憶體空間將丟擲OutOfMemoryError異常

        雖然把異常分為兩種情況看似更加嚴謹但是卻存在互相重疊的地方,當棧空間無法繼續分配的時候究竟是記憶體太小問題還是棧空間使用過多,在單執行緒的嘗試中均無法讓虛擬機器產生OutOfMemory異常,獲得的都是StackOverflowError異常,再單個執行緒下無論是因為棧幀過大還是虛擬機器棧容量太小,當記憶體無法分配的時候虛擬機器丟擲的都是StackOverflowError異常,虛擬機器丟擲的都是StackOverflowError異常。如果測試的時候不限於單執行緒,通過不斷的建立執行緒的方式倒是可以產生記憶體溢位異常,在這種情況下為每個執行緒的棧分配的記憶體越大,反而越容易產生記憶體溢位異常。

        原因就是作業系統分配給每個程序的記憶體是有限制的,虛擬機器提供了引數來控制java堆和方法區的這兩部分記憶體的最大值,剩餘記憶體減去最大堆容量以及最大方法區容量,程式計數器消耗記憶體可以忽略並且虛擬機器程序本身所佔的記憶體不在計算之內,剩下的記憶體就由本地方法棧和虛擬機器棧瓜分,每個執行緒分配到的棧容量越大可以建立的執行緒數量自然就越少,建立執行緒時就容易把剩下的記憶體耗盡。

        棧深度在大多數情況下達到1000~2000完全沒有問題,對於正常的方法呼叫這個深度足夠使用,但是在建立多執行緒導致的記憶體溢位在不能減少執行緒數或者更換64位虛擬機器的情況下,就只能減少最大堆容量和減少棧容量來換取更多執行緒。

 

方法區和執行時常量池溢位

        執行時常量池是方法區的一部分所以這兩個放在一起,說到常量池就要說到一個方法那就是String.intern(),這個方法是一個Native方法它的作用是如果字串常量池以及包含一個等於此String物件的字串,則返回代表池中並且返回此String物件的引用,在1.6版本之前的版本中由於常量池分配在永久代可以通過引數限制方法區大小,從而間接限制其中常量池的容量,當執行時常量池溢位時會給出PermGen space說明執行時常量池屬於方法區,但是使用1.7就不會出現這種情況(因為1.7之後開始逐漸去永久代)。

public static void main(String[] args){

    String str1 =new StringBulider("hhh").append("ooo").toString();

    System.out.println(str1.intern()==str1);

 

    String str2 = new StringBulider("ja").append("va").toString();

    System.out.println(str2.,intern() == str2);

}

      這段程式碼在jdk1.7和jdk1.6中執行得到的結果是不一樣的,1.6版本中會得到兩個false而在1.7會得到一個true和一個false。因為在1.6中intern()方法會把首次遇到的字串例項複製到永久代中返回的也是永久代中的這個字串例項的引用,而由StringBulider建立的字串例項實際在堆中所以不是同一個引用將返回false,而1.7版本的intern()的實現不會在複製例項,只是在常量池中記錄首次出現的例項引用,因此intern()和StringBuilder建立的例項是一個,對str2比較返回false的原因是java這個字串在執行StringBulider.toString()之前出現過了,字串常量池中已經有了它的引用不符合首次出現的原則。

 

      方法區用於存放Class的資訊,對於該區的測試基本上就是執行時產生大量的類去填滿方法區,可以通過GGlib操作位元組碼直接在執行時生成大量的動態類。在當前的主流框架中,如Spring,Hibernate對類進行加強時都會使用GGlib這類位元組碼技術,方法區溢位也是一種常見的記憶體溢位異常,一個類要被垃圾收集器回收掉判定條件比較苛刻。

 

本機直接記憶體溢位

       DirectMemory容量可以通過MaxDirectMemorySize指定,如果不指定則預設與java最大值一樣,直接通過反射獲取Unsafe例項進行記憶體分配,因為使用DirectByteBuffer分配記憶體也會丟擲記憶體溢位異常,但是丟擲異常時並沒有真正向作業系統申請分配記憶體而是計算得知記憶體無法分配於是手動丟擲異常,真正申請分配記憶體的方法是unsafe.allocateMemory(),由DirectMemory導致的記憶體溢位,一個明顯的特徵是在Heap Dump檔案中不會有明顯的異常,如果OOM之後發現Dump檔案很小而程式中又直接或者間接使用了NIO就可以考慮是不是直接記憶體溢位了。