JVM系列之六:內存溢出、內存泄漏 和 棧溢出


1. OOM && SOF

OutOfMemoryError異常: 除了程序計數器外,虛擬機內存的其他幾個運行時區域都有發生OutOfMemoryError(OOM)異常的可能,

內存泄露:指程序中動態分配內存給一些臨時對象,但是對象不會被GC所回收,它始終占用內存。即被分配的對象可達但已無用。

內存溢出指程序運行過程中無法申請到足夠的內存而導致的一種錯誤。內存溢出通常發生於OLD段或Perm段垃圾回收后,仍然無內存空間容納新的Java對象的情況。

從定義上可以看出內存泄露是內存溢出的一種誘因,不是唯一因素

棧溢出當應用程序遞歸太深而發生堆棧溢出時,拋出該錯誤。 

2. 發生了內存泄露或溢出怎么辦?

一般的異常信息:java.lang.OutOfMemoryError:Java heap spacess
  java堆用於存儲對象實例,我們只要不斷的創建對象,並且保證GC Roots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,就會在對象數量達到最大堆容量限制后產生內存溢出異常。

(1)通過參數 -XX:+HeapDumpOnOutOfMemoryError 讓虛擬機在出現OOM異常的時候Dump出內存映像以便於分析。

(2)一般手段是先通過內存映像分析工具(如Eclipse Memory Analyzer)對dump出來的堆轉存快照進行分析,重點是確認內存中的對象是否是必要的,先分清是因為內存泄漏(Memory Leak)還是內存溢出(Memory Overflow)。(到底是出現了內存泄漏還是內存溢出

哪些對象被懷疑為內存泄漏,哪些對象占的空間最大及對象的調用關系,還可以分析線程狀態,可以觀察到線程被阻塞在哪個對象上,從而判斷系統的瓶頸。

(3)如果是內存泄漏,可進一步通過工具查看泄漏對象到GC Roots的引用鏈。於是就能找到泄漏對象時通過怎樣的路徑與GC Roots相關聯並導致垃圾收集器無法自動回收。 找到引用信息,可以准確的定位出內存泄漏的代碼位置。(HashMap中的元素的某些屬性改變了,影響了hashcode的值會發生內存泄漏)

(4)如果不存在內存泄漏,就應當檢查虛擬機的參數(-Xmx與-Xms)的設置是否適當,是否可以調大;修改代碼邏輯,把某些對象生命周期過長,持有狀態時間過長等情況的代碼修改。

 

3. 內存泄漏的場景

(1)使用靜態的集合類

  靜態的集合類的生命周期和應用程序的生命周期一樣長,所以在程序結束前容器中的對象不能被釋放,會造成內存泄露。

  解決辦法是最好不使用靜態的集合類,如果使用的話,在不需要容器時要將其賦值為null

  修改hashset中對象的參數值,且參數是計算哈希值的字段 

(2)單例模式可能會造成內存泄露(長生命周期的對象持有短生命周期對象的引用)

  單例模式只允許應用程序存在一個實例對象,並且這個實例對象的生命周期和應用程序的生命周期一樣長,如果單例對象中擁有另一個對象的引用的話,這個被引用的對象就不能被及時回收。

  解決辦法是單例對象中持有的其他對象使用弱引用,弱引用對象在GC線程工作時,其占用的內存會被回收掉。

(3)數據庫、網絡、輸入輸出流,這些資源沒有顯示的關閉

    垃圾回收只負責內存回收,如果對象正在使用資源的話,Java虛擬機不能判斷這些對象是不是正在進行操作,比如輸入輸出,也就不能回收這些對象占用的內存,所以在資源使用完后要調用close()方法關閉。

 

4. 內存溢出的場景

4.1 Java Heap 溢出

在jvm規范中,堆中的內存是用來生成對象實例和數組的。 
  如果細分,堆內存還可以分為年輕代和年老代,年輕代包括一個eden區和兩個survivor區。 
  當生成新對象時,內存的申請過程如下:

  1.  jvm先嘗試在eden區分配新建對象所需的內存;
  2.  如果內存大小足夠,申請結束,否則下一步;
  3.  jvm啟動youngGC,試圖將eden區中不活躍的對象釋放掉,釋放后若Eden空間仍然不足以放入新對象,則試圖將部分Eden中活躍對象放入Survivor區;
  4.  Survivor區被用來作為Eden及old的中間交換區域,當OLD區空間足夠時,Survivor區的對象會被移到Old區,否則會被保留在Survivor區;
  5.  當OLD區空間不夠時,JVM會在OLD區進行full GC;
  6.  full GC后,若Survivor及OLD區仍然無法存放從Eden復制過來的部分對象,導致JVM無法在Eden區為新對象創建內存區域,則出現”out of memory錯誤”: outOfMemoryError:java heap space

 

4.2 虛擬機棧和本地方法棧溢出   

  1. 如果線程請求的棧深度大於虛擬機所允許的最大深度,將拋出StackOverflowError異常。   
  2. 不斷創建線程,如果虛擬機在擴展棧時無法申請到足夠的內存空間,則拋出OutOfMemoryError異常  
  3. 這里需要注意當棧的大小越大可分配的線程數就越少。
  4. 用Xss設置

 

4.3 運行時常量池溢出

  1. 異常信息:java.lang.OutOfMemoryError:PermGen space
  2. 如果要向運行時常量池中添加內容,最簡單的做法就是使用String.intern()這個Native方法。
  3. 該方法的作用是:如果池中已經包含一個等於此String的字符串,則返回代表池中這個字符串的String對象;否則,將此String對象包含的字符串添加到常量池中,並且返回此String對象的引用。
  4. 由於常量池分配在方法區內,我們可以通過-XX:PermSize和-XX:MaxPermSize限制方法區的大小,從而間接限制其中常量池的容量。

 

4.4 方法區溢出

  異常信息:java.lang.OutOfMemoryError: PermGen space
    方法區用於存放Class的相關信息,如類名、訪問修飾符、常量池、字段描述、方法描述等。

  所以如果程序加載的類過多,或者使用反射、gclib等這種動態代理生成類的技術,就可能導致該區發生內存溢出
  方法區溢出也是一種常見的內存溢出異常,一個類如果要被垃圾收集器回收,判定條件是很苛刻的。在經常動態生成大量Class的應用中,要特別注意這點。 
  我們可以通過-XX:PermSize和-XX:MaxPermSize限制方法區的大小

4.5 java.lang.OutOfMemoryError: GC overhead limit exceeded

  原因:執行垃圾收集的時間比例太大, 有效的運算量太小. 默認情況下, 如果GC花費的時間超過 98%, 並且GC回收的內存少於 2%, JVM就會拋出這個錯誤。
  目的是為了讓應用終止,給開發者機會去診斷問題。一般是應用程序在有限的內存上創建了大量的臨時對象或者弱引用對象,從而導致該異常
  解決方法:
    1. 大對象在使用之后指向null。
    2. 增加參數,-XX:-UseGCOverheadLimit,關閉這個特性;
    3. 增加heap大小,-Xmx1024m

 

5. SOF (堆棧溢出 StackOverflow)

  StackOverflowError 的定義:當應用程序遞歸太深而發生堆棧溢出時,拋出該錯誤。 因為棧一般默認為1-2M,一旦出現死循環或者是大量的遞歸調用,在不斷的壓棧過程中,造成棧容量超過1M而導致溢出。 
  棧溢出的原因: 
    遞歸調用 
    大量循環或死循環 
    全局變量是否過多 
    數組、List、map數據過大

 

6. 如何避免發生內存泄露和溢出

1、盡早釋放無用對象的引用

2、使用字符串處理,避免使用String,應大量使用StringBuffer,每一個String對象都得獨立占用內存一塊區域

3、盡量少用靜態變量,因為靜態變量存放在永久代(方法區),永久代基本不參與垃圾回收

4、避免在循環中創建對象

5、開啟大型文件或從數據庫一次拿了太多的數據很容易造成內存溢出,所以在這些地方要大概計算一下數據量的最大值是多少,並且設定所需最小及最大的內存空間值。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM