1. 程式人生 > >一段死迴圈引發的Java heap space型別的OutOfMemory

一段死迴圈引發的Java heap space型別的OutOfMemory

        7月份,公司各部門都開始進行績效考核,我們一個產品使用者需要從系統中匯出資料進行績效考核的依據。系統已經正常運行了一段時間了,這個匯出功能一直是正常的。但是使用者在匯出7月份的資料時,系統直接掛了。         使用者把問題反饋到研發這邊,到生產環境匯出嘗試了下,點選匯出按鈕後不到1分鐘,直接丟擲了記憶體溢位的異常資訊。這是一個維護專案,匯出沒有使用buffer的處理方式來防止大資料匯出時的記憶體溢位。所以看到記憶體溢位的異常,首先想到的就是是不是7月份的匯出資料量太大了?於是到樣本庫上查詢7月份的數量才區區幾千條資料,這也能導致記憶體溢位?雖然有了這個疑問,但還是沿著這個思路去驗證了這個匯出功能在匯出時的記憶體消耗情況。具體驗證的資訊如下: 驗證工具:jvisualvm jvm記憶體配置情況:-Xms512M -Xmx512M 匯出資料量:1W條 結果:匯出都正常 測試環境下JVM的記憶體在512M的情況下都能夠支撐1W條資料的匯出,生產環境下單節點的JVM配置都是 -Xms4096M -Xmx4096M,區區幾千條資料怎麼會導致記憶體溢位呢?         沒辦法,原本不想看這個維護專案的具體程式碼的,只能硬著頭皮看程式碼,看能不能從程式碼層面找出一些錯誤的端倪。這裡要謝謝東碩的提醒看具體的程式碼中是否有可能存在死迴圈的語句。朝著這個思路,很快看到一段程式碼從邏輯上有可能導致死迴圈:         只要當needAdd<0的場合,while迴圈就永遠迴圈了。一旦出現了死迴圈,serverTypeList物件會不斷的被複制,從而導致serverTypeList物件瞬間飆大,進而導致jvm需要進行full gc將年輕代記憶體移動到年老大記憶體,但是儘管進行了full gc 但是年輕代記憶體大小仍然滿足不了serverTypeList的大小。這時候就會jvm則會丟擲OutOfMemoryError: Java heap space的異常了。         接下來找DBA幫忙把7月份樣本庫資料同步到我們測試環境,然後在測試環境中驗證我們上面的推測。接下來一共進行了10次匯出測試,一共出現了3次記憶體溢位,其餘八次程式並沒有出現記憶體溢位,但是jvm非常頻繁的進行full gc。這裡基本上已經驗證了記憶體溢位是因為這段程式碼出現死迴圈導致的。         但是這裡還是有一個疑問?生產環境下每次匯出7月份的資料時很快就會出現記憶體溢位,但是測試環境下為什麼有7次情況下full gc能夠非常頻繁的進行而程式沒有最終報記憶體溢位異常呢?         對比生產環境和測試環境jvm配置:        生產環境: -Xms4096M -Xmx4096M    每次都出現記憶體溢位        測試環境1:-Xms1024M -Xmx1024M   兩次出現記憶體溢位         測試環境2:-Xms512M -Xmx512M        一次出現記憶體溢位、7次未出現記憶體溢位 推測:jvm堆記憶體越大,full gc更新頻率越低,full gc的時間間隔太長導致不能通過頻繁的full gc避免記憶體溢            出。這邊歡迎有小夥伴能夠給出更好的解釋。 *******************************一篇關於Full GC的參考文章******************************* 觸發JVM進行Full GC的情況及應對策略

堆記憶體劃分為 Eden、Survivor 和 Tenured/Old 空間,如下圖所示:

從年輕代空間(包括 Eden 和 Survivor 區域)回收記憶體被稱為 Minor GC,對老年代GC稱為Major GC,而Full GC是對整個堆來說的,在最近幾個版本的JDK裡預設包括了對永生帶即方法區的回收(JDK8中無永生帶了),出現Full GC的時候經常伴隨至少一次的Minor GC,但非絕對的。Major GC的速度一般會比Minor GC慢10倍以上。下邊看看有那種情況觸發JVM進行Full GC及應對策略。

1、System.gc()方法的呼叫

 此方法的呼叫是建議JVM進行Full GC,雖然只是建議而非一定,但很多情況下它會觸發 Full GC,從而增加Full GC的頻率,也即增加了間歇性停頓的次數。強烈影響系建議能不使用此方法就別使用,讓虛擬機器自己去管理它的記憶體,可通過通過-XX:+ DisableExplicitGC來禁止RMI呼叫System.gc。


2、老年代代空間不足

老年代空間只有在新生代物件轉入及建立為大物件、大陣列時才會出現不足的現象,當執行Full GC後空間仍然不足,則丟擲如下錯誤:
java.lang.OutOfMemoryError: Java heap space 
為避免以上兩種狀況引起的Full GC,調優時應儘量做到讓物件在Minor GC階段被回收、讓物件在新生代多存活一段時間及不要建立過大的物件及陣列。

3、永生區空間不足

JVM規範中執行時資料區域中的方法區,在HotSpot虛擬機器中又被習慣稱為永生代或者永生區,Permanet Generation中存放的為一些class的資訊、常量、靜態變數等資料,當系統中要載入的類、反射的類和呼叫的方法較多時,Permanet Generation可能會被佔滿,在未

配置為採用CMS GC的情況下也會執行Full GC。如果經過Full GC仍然回收不了,那麼JVM會丟擲如下錯誤資訊:
java.lang.OutOfMemoryError: PermGen space 
為避免Perm Gen佔滿造成Full GC現象,可採用的方法為增大Perm Gen空間或轉為使用CMS GC。


4、CMS GC時出現promotion failed和concurrent mode failure

對於採用CMS進行老年代GC的程式而言,尤其要注意GC日誌中是否有promotion failed和concurrent mode failure兩種狀況,當這兩種狀況出現時可能

會觸發Full GC。
promotion failed是在進行Minor GC時,survivor space放不下、物件只能放入老年代,而此時老年代也放不下造成的;concurrent mode failure是在

執行CMS GC的過程中同時有物件要放入老年代,而此時老年代空間不足造成的(有時候“空間不足”是CMS GC時當前的浮動垃圾過多導致暫時性的空間不足觸發Full GC)。
對措施為:增大survivor space、老年代空間或調低觸發併發GC的比率,但在JDK 5.0+、6.0+的版本中有可能會由於JDK的bug29導致CMS在remark完畢

後很久才觸發sweeping動作。對於這種狀況,可通過設定-XX: CMSMaxAbortablePrecleanTime=5(單位為ms)來避免。


5、統計得到的Minor GC晉升到舊生代的平均大小大於老年代的剩餘空間

這是一個較為複雜的觸發情況,Hotspot為了避免由於新生代物件晉升到舊生代導致舊生代空間不足的現象,在進行Minor GC時,做了一個判斷,如果之

前統計所得到的Minor GC晉升到舊生代的平均大小大於舊生代的剩餘空間,那麼就直接觸發Full GC。
例如程式第一次觸發Minor GC後,有6MB的物件晉升到舊生代,那麼當下一次Minor GC發生時,首先檢查舊生代的剩餘空間是否大於6MB,如果小於6MB,

則執行Full GC。
當新生代採用PS GC時,方式稍有不同,PS GC是在Minor GC後也會檢查,例如上面的例子中第一次Minor GC後,PS GC會檢查此時舊生代的剩餘空間是否

大於6MB,如小於,則觸發對舊生代的回收。
除了以上4種狀況外,對於使用RMI來進行RPC或管理的Sun JDK應用而言,預設情況下會一小時執行一次Full GC。可通過在啟動時通過- java -

Dsun.rmi.dgc.client.gcInterval=3600000來設定Full GC執行的間隔時間或通過-XX:+ DisableExplicitGC來禁止RMI呼叫System.gc。

6、堆中分配很大的物件

 所謂大物件,是指需要大量連續記憶體空間的java物件,例如很長的陣列,此種物件會直接進入老年代,而老年代雖然有很大的剩餘空間,但是無法找到足夠大的連續空間來分配給當前物件,此種情況就會觸發JVM進行Full GC。

為了解決這個問題,CMS垃圾收集器提供了一個可配置的引數,即-XX:+UseCMSCompactAtFullCollection開關引數,用於在“享受”完Full GC服務之後額外免費贈送一個碎片整理的過程,記憶體整理的過程無法併發的,空間碎片問題沒有了,但提頓時間不得不變長了,JVM設計者們還提供了另外一個引數 -XX:CMSFullGCsBeforeCompaction,這個引數用於設定在執行多少次不壓縮的Full GC後,跟著來一次帶壓縮的。