一、前言
- →_→嗯....基本為《深入理解java虛擬機(jī)》的筆記。
- 本篇目的:在工作中遇到實(shí)際的內(nèi)存溢出異常時,能根據(jù)異常的信息快速判斷是哪個區(qū)域的內(nèi)存溢出,知道什么樣的代碼可能會導(dǎo)致這些區(qū)域內(nèi)存溢出,以及出現(xiàn)這些異常后該如何處理。本文通過"測試-分析-處理"的流程來整理知識。
二、各種溢出異常
1. java堆溢出=============================================
??Java堆用于存儲對象實(shí)例,只要不斷的創(chuàng)建對象,并且保證GC Roots到對象之間有可達(dá)路徑來避免垃圾回收機(jī)制清除這些對象,那么在數(shù)量到達(dá)最大堆的容量限制后就會產(chǎn)生內(nèi)存溢出異常。
??測試:將堆的大小限制為20MB,不可擴(kuò)展(將堆的最小值-Xms參數(shù)與最大值-Xmx參數(shù)設(shè)置為一樣即可避免堆自動擴(kuò)展),創(chuàng)建list對象并無限循環(huán)像list存入新的對象,運(yùn)行結(jié)果出現(xiàn)異常java.lang.OutOfMemoryError:Java heap space
??分析:一般的手段是先通過內(nèi)存映像分析工具(如Eclipse Memory Analyzer)對Dump出來的堆轉(zhuǎn)儲快照進(jìn)行分析,重點(diǎn)是確認(rèn)內(nèi)存中的對象是否是必要的,也就是先分清楚到底是出現(xiàn)了內(nèi)存泄露(Memory Leak)還是內(nèi)存溢出(Memory Overflow)。
??處理:
- 如果是內(nèi)存泄漏,可進(jìn)一步通過工具查看泄漏對象到GC Roots的引用鏈。于是就能找到泄露對象是通過怎樣的路徑與GC Roots相關(guān)聯(lián)并導(dǎo)致垃圾收集器無法自動回收它們的。掌握了泄漏對象的類型信息及GC Roots引用鏈的信息,就可以比較準(zhǔn)確地定位出泄漏代碼的位置。
- 如果不存在泄露,換句話說,就是內(nèi)存中的對象確實(shí)都還必須存活著,那就應(yīng)當(dāng)檢查虛擬機(jī)的堆參數(shù)(-Xmx與-Xms),與機(jī)器物理內(nèi)存對比看是否還可以調(diào)大,從代碼上檢查是否存在某些對象生命周期過長、持有狀態(tài)時間過長的情況,嘗試減少程序運(yùn)行期的內(nèi)存消耗。
2. 虛擬機(jī)棧和本地方法棧溢出=================================
??由于HotSpot虛擬機(jī)中并不區(qū)分虛擬機(jī)和本地方法棧,對于HotSpot來說,雖然-Xoss參數(shù)(設(shè)置本地方法棧大?。┐嬖冢珜?shí)際上是無效的,棧容量只由-Xss參數(shù)設(shè)定。關(guān)于虛擬機(jī)棧和本地方法棧,在Java虛擬機(jī)規(guī)范中描述了兩種異常:
- 如果線程請求的棧深度大于虛擬機(jī)所允許的最大深度,將拋出StackOverflowError
- 如果虛擬機(jī)在擴(kuò)展棧時無法申請到足夠的內(nèi)存空間,則拋出OutOfMemoryError異常
??測試:①使用-Xss參數(shù)減少棧內(nèi)存容量 ,運(yùn)行結(jié)果拋出StackOverflowError異常 , 異常出現(xiàn)時輸出的堆棧深度相應(yīng)縮小。②定義了大量的本地變量,增大此方法幀中本地變量表的長度,結(jié)果拋出運(yùn)行結(jié)果拋出StackOverflowError異常時輸出的堆棧深度相應(yīng)縮小。
??分析:在單線程下,無論由于棧幀太大還是虛擬機(jī)棧容量太小,當(dāng)內(nèi)存無法分配的時候,虛擬機(jī)拋出的都是StackOverflowError異常;如果是多線程導(dǎo)致的內(nèi)存溢出,與??臻g是否足夠大并不存在任何聯(lián)系,這個時候每個線程的棧分配的內(nèi)存越大,反而越容易產(chǎn)生內(nèi)存溢出異常。
??處理:解決的時候是在不能減少線程數(shù)或更換64為的虛擬機(jī)的情況下,就只能通過減少最大堆和減少棧容量來換取更多的線程
3. 方法區(qū)和運(yùn)行時常量池溢出=================================
運(yùn)行時常量池是方法區(qū)的一部分,在jdk1.6及之前的版本中,由于常量池分配在永久代中,可以通過-XX:PermSize和-XX:MaxPermSize限制方法區(qū)大小,從而間接限制其中常量池的容量。
String.intern()是一個Native方法,它的作用是:如果字符串常量池中已經(jīng)包含一個等于此String對象的字符串,則返回代表池中這個字符串的String對象;否則,將此String對象包含的字符串添加到常量池中,并且返回此String對象的引用。
??測試:


public static void main(String[] args) {
//intern返回常量池中記錄首次出現(xiàn)的實(shí)例
//String.intern()方法 當(dāng)調(diào)用 intern 方法時,如果池已經(jīng)包含一個等于此 String 對象的字符串(用 equals(Object) 方法確定),則返回池中的字符串。否則,將此 String 對象添加到池中,并返回此 String 對象的引用。
String str1 = new StringBuilder("計(jì)算機(jī)").append("軟件").toString();
System.out.println(str1.intern() == str1); //true
String str2 = new StringBuilder("ja").append("va").toString(); //其他像int,float,double,byte等也是將會返回false
System.out.println(str2.intern() == str2); //false StringBuilder.toString之前,字符串常量池里面已經(jīng)有了java這個字符串,不是首次出現(xiàn)
}
這段代碼在JDK1.6中運(yùn)行 , 會得到兩個false , 而在JDK1.7中,會得到一個true一個false.
??分析:
- JDK1.6 intern方法會把首次遇到的字符串實(shí)例復(fù)制到永久代,返回的也是永久代中這個字符串實(shí)例的引用,而由StringBuilder創(chuàng)建的字符串實(shí)例在Java堆上,所以必然不是一個引用。
- JDK1.7 intern()方法的實(shí)現(xiàn)不會再復(fù)制實(shí)例,只是在常量池中記錄首次出現(xiàn)的實(shí)例引用,因此intern()返回的引用和由StringBuilder創(chuàng)建的那個字符串實(shí)例是同一個。
??處理:方法區(qū)溢出也是一種常見的內(nèi)存溢出異常 , 一個類要被垃圾收集器回收掉 , 判定條件是比較苛刻的。在經(jīng)常動態(tài)生成大量的Class應(yīng)用中,需要特別注意類的回收狀況。常見的有 : 大量JSP或動態(tài)產(chǎn)生JSP文件的應(yīng)用(jsp第一次運(yùn)行時需要編譯為java類)、基于OSGi的應(yīng)用等
4. 本機(jī)直接內(nèi)存溢出=================================
直接內(nèi)存(Direct Memory)通過 -XX:MaxDirectMemorySize指定,不指定則默認(rèn)與Java堆最大值(-Xmx指定)一樣
??測試:

??分析:明顯特征是Heap Dump文件中不會看到明顯的異常
??處理:如果發(fā)現(xiàn)OOM之后Dump文件很小,而程序中又直接或間接使用了 NIO,那就可以考慮檢查一下是不是這方面的原因。