1. 程式人生 > >jvm出現OutOfMemoryError時處理方法/jvm原理和優化參考

jvm出現OutOfMemoryError時處理方法/jvm原理和優化參考

The heap stores all of the objects created by your java program.The heap's contents is monitored by the garbage collector,which frees memory from the heap when you stop using an object.
This is in contrast with the stack, which stores primitive types like ints and chars, and are typically local variables and function return values. Thess are not garbage collected.


Class在被 Load的時候被放入PermGen space區域,Instance存放在Heap區域
Java中關於OOM的場景及解決方法

1、OOM for Heap=>例如:java.lang.OutOfMemoryError: Java heap space
分  析
此OOM是由於JVM中heap的最大值不滿足需要,將設定heap的最大值調高即可,引數樣例為:-Xmx2G
解決方法
調高heap的最大值,即-Xmx的值調大。

2、OOM for Perm=>例如:java.lang.OutOfMemoryError: Java perm space
分  析
此OOM是由於JVM中perm的最大值不滿足需要,將設定perm的最大值調高即可,引數樣例為:-XX:MaxPermSize=512M
解決方法

調高heap的最大值,即-XX:MaxPermSize的值調大。
另外,注意一點,Perm一般是在JVM啟動時載入類進來,如果是JVM執行較長一段時間而不是剛啟動後溢位的話,很有可能是由於執行時有類被動態載入進來,此時建議用CMS策略中的類解除安裝配置。
如:-XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled

3、OOM for GC=>例如:java.lang.OutOfMemoryError: GC overhead limit exceeded
分  析
此OOM是由於JVM在GC時,物件過多,導致記憶體溢位,建議調整GC的策略,在一定比例下開始GC而不要使用預設的策略,或者將新代和老代設定合適的大小,需要進行微調存活率。
解決方法

改變GC策略,在老代80%時就是開始GC,並且將-XX:SurvivorRatio(-XX:SurvivorRatio=8)和-XX:NewRatio(-XX:NewRatio=4)設定的更合理

jvm原理和優化參考:

JVM工作原理和特點主要是指作業系統裝入JVM是通過jdk中Java.exe來完成,通過下面4步來完成JVM環境.

1.建立JVM裝載環境和配置

2.裝載JVM.dll

3.初始化JVM.dll並掛界到JNIENV(JNI呼叫介面)例項

4.呼叫JNIEnv例項裝載並處理class類。

在我們執行和除錯Java程式的時候,經常會提到一個JVM的概念.JVM是Java程式執行的環境,但是他同時一個作業系統的一個應用程式一個程序,因此他也有他自己的執行的生命週期,也有自己的程式碼和資料空間.

首先來說一下JVM工作原理中的jdk這個東西,不管你是初學者還是高手,是j2ee程式設計師還是j2se程式設計師,jdk總是在幫我們做一些事情.我們在瞭解Java之前首先大師們會給我們提供說jdk這個東西.它在Java整個體系中充當著什麼角色呢?我很驚歎sun大師們設計天才,能把一個如此完整的體系結構化的如此完美.jdk在這個體系中充當一個生產加工中心,產生所有的資料輸出,是所有指令和戰略的執行中心.本身它提供了Java的完整方案,可以開發目前Java能支援的所有應用和系統程式.這裡說一個問題,大家會問,那為什麼還有j2me,j2ee這些東西,這兩個東西目的很簡單,分別用來簡化各自領域內的開發和構建過程.jdk除了JVM之外,還有一些核心的API,整合API,使用者工具,開發技術,開發工具和API等組成

好了,廢話說了那麼多,來點於主題相關的東西吧.JVM在整個jdk中處於最底層,負責於作業系統的互動,用來遮蔽作業系統環境,提供一個完整的Java執行環境,因此也就虛擬計算機. 作業系統裝入JVM是通過jdk中Java.exe來完成,通過下面4步來完成JVM環境.

1.建立JVM裝載環境和配置

2.裝載JVM.dll

3.初始化JVM.dll並掛界到JNIENV(JNI呼叫介面)例項

4.呼叫JNIEnv例項裝載並處理class類。

一.JVM裝入環境,JVM提供的方式是作業系統的動態連線檔案.既然是檔案那就一個裝入路徑的問題,Java是怎麼找這個路徑的呢?當你在呼叫Java test的時候,作業系統會在path下在你的Java.exe程式,Java.exe就通過下面一個過程來確定JVM的路徑和相關的引數配置了.下面基於Windows的實現的分析.

首先查詢jre路徑,Java是通過GetApplicationHome api來獲得當前的Java.exe絕對路徑,c:\j2sdk1.4.2_09\bin\Java.exe,那麼它會擷取到絕對路徑c:\j2sdk1.4.2_09\,判斷c:\j2sdk1.4.2_09\bin\Java.dll檔案是否存在,如果存在就把c:\j2sdk1.4.2_09\作為jre路徑,如果不存在則判斷c:\j2sdk1.4.2_09\jre\bin\Java.dll是否存在,如果存在這c:\j2sdk1.4.2_09\jre作為jre路徑.如果不存在呼叫GetPublicJREHome查HKEY_LOCAL_MACHINE\Software\JavaSoft\Java Runtime Environment\“當前JRE版本號”\JavaHome的路徑為jre路徑。

然後裝載JVM.cfg檔案JRE路徑+\lib+\ARCH(CPU構架)+\JVM.cfgARCH(CPU構架)的判斷是通過Java_md.c中GetArch函式判斷的,該函式中windows平臺只有兩種情況:WIN64的‘ia64’,其他情況都為‘i386’。以我的為例:C:\j2sdk1.4.2_09\jre\lib\i386\JVM.cfg.主要的內容如下:

  1. -client KNOWN   
  2. -server KNOWN   
  3. -hotspot ALIASED_TO -client   
  4. -classic WARN   
  5. -native ERROR   
  6. -green ERROR  

在我們的jdk目錄中jre\bin\server和jre\bin\client都有JVM.dll檔案存在,而Java正是通過JVM.cfg配置檔案來管理這些不同版本的JVM.dll的.通過檔案我們可以定義目前jdk中支援那些JVM,前面部分(client)是JVM名稱,後面是引數,KNOWN表示JVM存在,ALIASED_TO表示給別的JVM取一個別名,WARN表示不存在時找一個JVM替代,ERROR表示不存在丟擲異常.在執行Java XXX是,Java.exe會通過CheckJVMType來檢查當前的JVM型別,Java可以通過兩種引數的方式來指定具體的JVM型別,一種按照JVM.cfg檔案中的JVM名稱指定,第二種方法是直接指定,它們執行的方法分別是“Java -J”、“Java -XXaltJVM=”或“Java -J-XXaltJVM=”。如果是第一種引數傳遞方式,CheckJVMType函式會取引數‘-J’後面的JVM名稱,然後從已知的JVM配置引數中查詢如果找到同名的則去掉該JVM名稱前的‘-’直接返回該值;而第二種方法,會直接返回“-XXaltJVM=”或“-J-XXaltJVM=”後面的JVM型別名稱;如果在執行Java時未指定上面兩種方法中的任一一種引數,CheckJVMType會取配置檔案中第一個配置中的JVM名稱,去掉名稱前面的‘-’返回該值。CheckJVMType函式的這個返回值會在下面的函式中匯同jre路徑組合成JVM.dll的絕對路徑。如果沒有指定這會使用JVM.cfg中第一個定義的JVM.可以通過set _Java_LAUNCHER_DEBUG=1在控制檯上測試.

最後獲得JVM.dll的路徑,JRE路徑+\bin+\JVM型別字串+\JVM.dll就是JVM的檔案路徑了,但是如果在呼叫Java程式時用-XXaltJVM=引數指定的路徑path,就直接用path+\JVM.dll檔案做為JVM.dll的檔案路徑.

二:裝載JVM.dll

通過第一步已經找到了JVM的路徑,Java通過LoadJavaVM來裝入JVM.dll檔案.裝入工作很簡單就是呼叫Windows API函式:

LoadLibrary裝載JVM.dll動態連線庫.然後把JVM.dll中的匯出函式JNI_CreateJavaVM和JNI_GetDefaultJavaVMInitArgs掛接到InvocationFunctions變數的CreateJavaVM和GetDefaultJavaVMInitArgs函式指標變數上。JVM.dll的裝載工作宣告完成。

三:初始化JVM,獲得本地呼叫介面,這樣就可以在Java中呼叫JVM的函數了.呼叫InvocationFunctions->CreateJavaVM也就是JVM中JNI_CreateJavaVM方法獲得JNIEnv結構的例項.

四:執行Java程式.

Java程式有兩種方式一種是jar包,一種是class. 執行jar,Java -jar XXX.jar執行的時候,Java.exe呼叫GetMainClassName函式,該函式先獲得JNIEnv例項然後呼叫Java類Java.util.jar.JarFileJNIEnv中方法getManifest()並從返回的Manifest物件中取getAttributes("Main-Class")的值即jar包中檔案:META-INF/MANIFEST.MF指定的Main-Class的主類名作為執行的主類。之後main函式會呼叫Java.c中LoadClass方法裝載該主類(使用JNIEnv例項的FindClass)。main函式直接呼叫Java.c中LoadClass方法裝載該類。如果是執行class方法。main函式直接呼叫Java.c中LoadClass方法裝載該類。

然後main函式呼叫JNIEnv例項的GetStaticMethodID方法查詢裝載的class主類中

“public static void main(String[] args)”方法,並判斷該方法是否為public方法,然後呼叫JNIEnv例項的

CallStaticVoidMethod方法呼叫該Java類的main方法。 

= GC 基礎 =====================

JAVA堆的描述如下:


記憶體由 Perm 和 Heap 組成. 其中
Heap = {Old + NEW = { Eden , from, to } }
JVM記憶體模型中分兩大塊,一塊是 NEW Generation, 另一塊是Old Generation. 在New Generation中,有一個叫Eden的空間,主要是用來存放新生的物件,還有兩個Survivor Spaces(from,to), 它們用來存放每次垃圾回收後存活下來的物件。在Old Generation中,主要存放應用程式中生命週期長的記憶體物件,還有個Permanent Generation,主要用來放JVM自己的反射物件,比如類物件和方法物件等。
垃圾回收描述:
在New Generation塊中,垃圾回收一般用Copying的演算法,速度快。每次GC的時候,存活下來的物件首先由Eden拷貝到某個Survivor Space, 當Survivor Space空間滿了後, 剩下的live物件就被直接拷貝到Old Generation中去。因此,每次GC後,Eden記憶體塊會被清空。在Old Generation塊中,垃圾回收一般用mark-compact的演算法,速度慢些,但減少記憶體要求.
垃圾回收分多級,0級為全部(Full)的垃圾回收,會回收OLD段中的垃圾;1級或以上為部分垃圾回收,只會回收NEW中的垃圾,記憶體溢位通常發生於OLD段或Perm段垃圾回收後,仍然無記憶體空間容納新的Java物件的情況。


當一個URL被訪問時,記憶體申請過程如下:
A. JVM會試圖為相關Java物件在Eden中初始化一塊記憶體區域
B. 當Eden空間足夠時,記憶體申請結束。否則到下一步
C. JVM試圖釋放在Eden中所有不活躍的物件(這屬於1或更高階的垃圾回收), 釋放後若Eden空間仍然不足以放入新物件,則試圖將部分Eden中活躍物件放入Survivor區
D. Survivor區被用來作為Eden及OLD的中間交換區域,當OLD區空間足夠時,Survivor區的物件會被移到Old區,否則會被保留在Survivor區
E. 當OLD區空間不夠時,JVM會在OLD區進行完全的垃圾收集(0級)
F. 完全垃圾收集後,若Survivor及OLD區仍然無法存放從Eden複製過來的部分物件,導致JVM無法在Eden區為新物件建立記憶體區域,則出現”out of memory錯誤”
JVM調優建議:
ms/mx:定義YOUNG+OLD段的總尺寸,ms為JVM啟動時YOUNG+OLD的記憶體大小;mx為最大可佔用的YOUNG+OLD記憶體大小。在使用者生產環境上一般將這兩個值設為相同,以減少執行期間系統在記憶體申請上所花的開銷。
NewSize/MaxNewSize:定義YOUNG段的尺寸,NewSize為JVM啟動時YOUNG的記憶體大小;MaxNewSize為最大可佔用的YOUNG記憶體大小。在使用者生產環境上一般將這兩個值設為相同,以減少執行期間系統在記憶體申請上所花的開銷。
PermSize/MaxPermSize:定義Perm段的尺寸,PermSize為JVM啟動時Perm的記憶體大小;MaxPermSize為最大可佔用的Perm記憶體大小。在使用者生產環境上一般將這兩個值設為相同,以減少執行期間系統在記憶體申請上所花的開銷。
SurvivorRatio:設定Survivor空間和Eden空間的比例
記憶體溢位的可能性


1. OLD段溢位
這種記憶體溢位是最常見的情況之一,產生的原因可能是:
1) 設定的記憶體引數過小(ms/mx, NewSize/MaxNewSize)
2) 程式問題
單個程式持續進行消耗記憶體的處理,如迴圈幾千次的字串處理,對字串處理應建議使用StringBuffer。此時不會報記憶體溢位錯,卻會使系統持續垃圾收集,無法處理其它請求,相關問題程式可通過Thread Dump獲取(見系統問題診斷一章)單個程式所申請記憶體過大,有的程式會申請幾十乃至幾百兆記憶體,此時JVM也會因無法申請到資源而出現記憶體溢位,對此首先要找到相關功能,然後交予程式設計師修改,要找到相關程式,必須在Apache日誌中尋找。
當Java物件使用完畢後,其所引用的物件卻沒有銷燬,使得JVM認為他還是活躍的物件而不進行回收,這樣累計佔用了大量記憶體而無法釋放。由於目前市面上還沒有對系統影響小的記憶體分析工具,故此時只能和程式設計師一起定位。
2. Perm段溢位
通常由於Perm段裝載了大量的Servlet類而導致溢位,目前的解決辦法:
1) 將PermSize擴大,一般256M能夠滿足要求
2) 若別無選擇,則只能將servlet的路徑加到CLASSPATH中,但一般不建議這麼處理


3. C Heap溢位
系統對C Heap沒有限制,故C Heap發生問題時,Java程序所佔記憶體會持續增長,直到佔用所有可用系統記憶體
其他:
JVM有2個GC執行緒。第一個執行緒負責回收Heap的Young區。第二個執行緒在Heap不足時,遍歷Heap,將Young 區升級為Older區。Older區的大小等於-Xmx減去-Xmn,不能將-Xms的值設的過大,因為第二個執行緒被迫執行會降低JVM的效能。
為什麼一些程式頻繁發生GC?有如下原因:
l         程式內呼叫了System.gc()或Runtime.gc()。
l         一些中介軟體軟體呼叫自己的GC方法,此時需要設定引數禁止這些GC。
l         Java的Heap太小,一般預設的Heap值都很小。
l         頻繁例項化物件,Release物件。此時儘量儲存並重用物件,例如使用StringBuffer()和String()。
如果你發現每次GC後,Heap的剩餘空間會是總空間的50%,這表示你的Heap處於健康狀態。許多Server端的Java程式每次GC後最好能有65%的剩餘空間。
經驗之談:
1.Server端JVM最好將-Xms和-Xmx設為相同值。為了優化GC,最好讓-Xmn值約等於-Xmx的1/3[2]。
2.一個GUI程式最好是每10到20秒間執行一次GC,每次在半秒之內完成[2]。
注意:
1.增加Heap的大小雖然會降低GC的頻率,但也增加了每次GC的時間。並且GC執行時,所有的使用者執行緒將暫停,也就是GC期間,Java應用程式不做任何工作。
2.Heap大小並不決定程序的記憶體使用量。程序的記憶體使用量要大於-Xmx定義的值,因為Java為其他任務分配記憶體,例如每個執行緒的Stack等。
2.Stack的設定
每個執行緒都有他自己的Stack。

-Xss 每個執行緒的Stack大小
Stack的大小限制著執行緒的數量。如果Stack過大就好導致記憶體溢漏。-Xss引數決定Stack大小,例如-Xss1024K。如果Stack太小,也會導致Stack溢漏。
3.硬體環境
硬體環境也影響GC的效率,例如機器的種類,記憶體,swap空間,和CPU的數量。
如果你的程式需要頻繁建立很多transient物件,會導致JVM頻繁GC。這種情況你可以增加機器的記憶體,來減少Swap空間的使用[2]。
4.4種GC
第一種為單執行緒GC,也是預設的GC。,該GC適用於單CPU機器。
第二種為Throughput GC,是多執行緒的GC,適用於多CPU,使用大量執行緒的程式。第二種GC與第一種GC相似,不同在於GC在收集Young區是多執行緒的,但在Old區和第一種一樣,仍然採用單執行緒。-XX:+UseParallelGC引數啟動該GC。
第三種為Concurrent Low Pause GC,類似於第一種,適用於多CPU,並要求縮短因GC造成程式停滯的時間。這種GC可以在Old區的回收同時,執行應用程式。-XX:+UseConcMarkSweepGC引數啟動該GC。
第四種為Incremental Low Pause GC,適用於要求縮短因GC造成程式停滯的時間。這種GC可以在Young區回收的同時,回收一部分Old區物件。-Xincgc引數啟動該GC。

按照基本回收策略分

引用計數(Reference Counting):

比較古老的回收演算法。原理是此物件有一個引用,即增加一個計數,刪除一個引用則減少一個計數。垃圾回收時,只用收集計數為0的物件。此演算法最致命的是無法處理迴圈引用的問題。

標記-清除(Mark-Sweep):

 標記清楚

此演算法執行分兩階段。第一階段從引用根節點開始標記所有被引用的物件,第二階段遍歷整個堆,把未標記的物件清除。此演算法需要暫停整個應用,同時,會產生記憶體碎片。

複製(Copying):

複製 

此演算法把記憶體空間劃為兩個相等的區域,每次只使用其中一個區域。垃圾回收時,遍歷當前使用區域,把正在使用中的物件複製到另外一個區域中。演算法每次只處理正在使用中的物件,因此複製成本比較小,同時複製過去以後還能進行相應的記憶體整理,不會出現“碎片”問題。當然,此演算法的缺點也是很明顯的,就是需要兩倍記憶體空間。

標記-整理(Mark-Compact):

 標記整理

此演算法結合了“標記-清除”和“複製”兩個演算法的優點。也是分兩階段,第一階段從根節點開始標記所有被引用物件,第二階段遍歷整個堆,把清除未標記物件並且把存活物件“壓縮”到堆的其中一塊,按順序排放。此演算法避免了“標記-清除”的碎片問題,同時也避免了“複製”演算法的空間問題。

按分割槽對待的方式分

增量收集(Incremental Collecting):實時垃圾回收演算法,即:在應用進行的同時進行垃圾回收。不知道什麼原因JDK5.0中的收集器沒有使用這種演算法的。

分代收集(Generational Collecting):基於對物件生命週期分析後得出的垃圾回收演算法。把物件分為年青代、年老代、持久代,對不同生命週期的物件使用不同的演算法(上述方式中的一個)進行回收。現在的垃圾回收器(從J2SE1.2開始)都是使用此演算法的。

按系統執行緒分

序列收集:序列收集使用單執行緒處理所有垃圾回收工作,因為無需多執行緒互動,實現容易,而且效率比較高。但是,其侷限性也比較明顯,即無法使用多處理器的優勢,所以此收集適合單處理器機器。當然,此收集器也可以用在小資料量(100M左右)情況下的多處理器機器上。

並行收集:並行收集使用多執行緒處理垃圾回收工作,因而速度快,效率高。而且理論上CPU數目越多,越能體現出並行收集器的優勢。(串型收集的併發版本,需要暫停jvm) 並行paralise指的是多個任務在多個cpu中一起並行執行,最後將結果合併。效率是N倍。

併發收集:相對於序列收集和並行收集而言,前面兩個在進行垃圾回收工作時,需要暫停整個執行環境,而只有垃圾回收程式在執行,因此,系統在垃圾回收時會有明顯的暫停,而且暫停時間會因為堆越大而越長。(和並行收集不同,併發只有在開頭和結尾會暫停jvm)併發concurrent指的是多個任務在一個cpu偽同步執行,但其實是序列排程的,效率並非直接是N倍。

分代垃圾回收

    分代的垃圾回收策略,是基於這樣一個事實:不同的物件的生命週期是不一樣的。因此,不同生命週期的物件可以採取不同的收集方式,以便提高回收效率。

    在Java程式執行的過程中,會產生大量的物件,其中有些物件是與業務資訊相關,比如Http請求中的Session物件、執行緒、Socket連線,這類物件跟業務直接掛鉤,因此生命週期比較長。但是還有一些物件,主要是程式執行過程中生成的臨時變數,這些物件生命週期會比較短,比如:String物件,由於其不變類的特性,系統會產生大量的這些物件,有些物件甚至只用一次即可回收。

    試想,在不進行物件存活時間區分的情況下,每次垃圾回收都是對整個堆空間進行回收,花費時間相對會長,同時,因為每次回收都需要遍歷所有存活物件,但實際上,對於生命週期長的物件而言,這種遍歷是沒有效果的,因為可能進行了很多次遍歷,但是他們依舊存在。因此,分代垃圾回收採用分治的思想,進行代的劃分,把不同生命週期的物件放在不同代上,不同代上採用最適合它的垃圾回收方式進行回收。

jvm分代

如圖所示:

    虛擬機器中的共劃分為三個代:年輕代(Young Generation)、年老點(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java類的類資訊,與垃圾收集要收集的Java物件關係不大。年輕代和年老代的劃分是對垃圾收集影響比較大的。

年輕代:

    所有新生成的物件首先都是放在年輕代的。年輕代的目標就是儘可能快速的收集掉那些生命週期短的物件。年輕代分三個區。一個Eden區,兩個Survivor區(一般而言)。大部分物件在Eden區中生成。當Eden區滿時,還存活的物件將被複制到Survivor區(兩個中的一個),當這個Survivor區滿時,此區的存活物件將被複制到另外一個Survivor區,當這個Survivor區也滿了的時候,從第一個Survivor區複製過來的並且此時還存活的物件,將被複制“年老區(Tenured)”。需要注意,Survivor的兩個區是對稱的,沒先後關係,所以同一個區中可能同時存在從Eden複製過來 物件,和從前一個Survivor複製過來的物件,而複製到年老區的只有從第一個Survivor去過來的物件。而且,Survivor區總有一個是空的。同時,根據程式需要,Survivor區是可以配置為多個的(多於兩個),這樣可以增加物件在年輕代中的存在時間,減少被放到年老代的可能。

年老代:

    在年輕代中經歷了N次垃圾回收後仍然存活的物件,就會被放到年老代中。因此,可以認為年老代中存放的都是一些生命週期較長的物件。

持久代:

    用於存放靜態檔案,如今Java類、方法等。持久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者呼叫一些class,例如Hibernate等,在這種時候需要設定一個比較大的持久代空間來存放這些執行過程中新增的類。持久代大小通過-XX:MaxPermSize=<N>進行設定。

什麼情況下觸發垃圾回收 

由於物件進行了分代處理,因此垃圾回收區域、時間也不一樣。GC有兩種型別:Scavenge GCFull GC

Scavenge GC

    一般情況下,當新物件生成,並且在Eden申請空間失敗時,就會觸發Scavenge GC,對Eden區域進行GC,清除非存活物件,並且把尚且存活的物件移動到Survivor區。然後整理Survivor的兩個區。這種方式的GC是對年輕代的Eden區進行,不會影響到年老代。因為大部分物件都是從Eden區開始的,同時Eden區不會分配的很大,所以Eden區的GC會頻繁進行。因而,一般在這裡需要使用速度快、效率高的演算法,使Eden去能儘快空閒出來。

Full GC

    對整個堆進行整理,包括Young、Tenured和Perm。Full GC因為需要對整個對進行回收,所以比Scavenge GC要慢,因此應該儘可能減少Full GC的次數。在對JVM調優的過程中,很大一部分工作就是對於FullGC的調節。有如下原因可能導致Full GC:

· 年老代(Tenured)被寫滿

· 持久代(Perm)被寫滿 

· System.gc()被顯示呼叫 

·上一次GC之後Heap的各域分配策略動態變化

分代垃圾回收流程1

分代垃圾回收流程2

分代垃圾回收流程3

分代垃圾回收流程

= G1 ===================================

傳說中的G1,傳說中的low-pause垃圾收集。Java SE 6的update14版本中已經包含測試版,可以在啟動時加JVM引數來啟用

-XX:+UnlockExperimentalVMOptions -XX:+UseG1GC

本文摘自《構建高效能的大型分散式Java應用》一書,Garbage First簡稱G1,它的目標是要做到儘量減少GC所導致的應用暫停的時間,讓應用達到準實時的效果,同時保持JVM堆空間的利用率,將作為CMS的替代者在JDK 7中閃亮登場,其最大的特色在於允許指定在某個時間段內GC所導致的應用暫停的時間最大為多少,例如在100秒內最多允許GC導致的應用暫停時間為1秒,這個特性對於準實時響應的系統而言非常的吸引人,這樣就再也不用擔心繫統突然會暫停個兩三秒了。

G1要做到這樣的效果,也是有前提的,一方面是硬體環境的要求,必須是多核的CPU以及較大的記憶體(從規範來看,512M以上就滿足條件了),另外一方面是需要接受吞吐量的稍微降低,對於實時性要求高的系統而言,這點應該是可以接受的。

為了能夠達到這樣的效果,G1在原有的各種GC策略上進行了吸收和改進,在G1中可以看到增量收集器和CMS的影子,但它不僅僅是吸收原有GC策略的優點,並在此基礎上做出了很多的改進,簡單來說,G1吸收了增量GC以及CMS的精髓,將整個jvm Heap劃分為多個固定大小的region,掃描時採用Snapshot-at-the-beginning的併發marking演算法(具體在後面內容詳細解釋)對整個heap中的region進行mark,回收時根據region中活躍物件的bytes進行排序,首先回收活躍物件bytes小以及回收耗時短(預估出來的時間)的region,回收的方法為將此region中的活躍物件複製到另外的region中,根據指定的GC所能佔用的時間來估算能回收多少region,這點和以前版本的Full GC時得處理整個heap非常不同,這樣就做到了能夠儘量短時間的暫停應用,又能回收記憶體,由於這種策略在回收時首先回收的是垃圾物件所佔空間最多的region,因此稱為Garbage First。

看完上面對於G1策略的簡短描述,並不能清楚的掌握G1,在繼續詳細看G1的步驟之前,必須先明白G1對於JVM Heap的改造,這些對於習慣了劃分為new generation、old generation的大家來說都有不少的新意。

G1將Heap劃分為多個固定大小的region,這也是G1能夠實現控制GC導致的應用暫停時間的前提,region之間的物件引用通過remembered set來維護,每個region都有一個remembered set,remembered set中包含了引用當前region中物件的region的物件的pointer,由於同時應用也會造成這些region中物件的引用關係不斷的發生改變,G1採用了Card Table來用於應用通知region修改remembered sets,Card Table由多個512位元組的Card構成,這些Card在Card Table中以1個位元組來標識,每個應用的執行緒都有一個關聯的remembered set log,用於快取和順序化執行緒執行時造成的對於card的修改,另外,還有一個全域性的filled RS buffers,當應用執行緒執行時修改了card後,如果造成的改變僅為同一region中的物件之間的關聯,則不記錄remembered set log,如造成的改變為跨region中的物件的關聯,則記錄到執行緒的remembered set log,如執行緒的remembered set log滿了,則放入全域性的filled RS buffers中,執行緒自身則重新建立一個新的remembered set log,remembered set本身也是一個由一堆cards構成的雜湊表。

儘管G1將Heap劃分為了多個region,但其預設採用的仍然是分代的方式,只是僅簡單的劃分為了年輕代(young)和非年輕代,這也是由於G1仍然堅信大多數新建立的物件都是不需要長的生命週期的,對於應用新建立的物件,G1將其放入標識為young的region中,對於這些region,並不記錄remembered set logs,掃描時只需掃描活躍的物件,G1在分代的方式上還可更細的劃分為:fully young或partially young,fully young方式暫停的時候僅處理young regions,partially同樣處理所有的young regions,但它還會根據允許的GC的暫停時間來決定是否要加入其他的非young regions,G1是執行到fully-young方式還是partially young方式,外部是不能決定的,在啟動時,G1採用的為fully-young方式,當G1完成一次Concurrent Marking後,則切換為partially young方式,隨後G1跟蹤每次回收的效率,如果回收fully-young中的regions已經可以滿足記憶體需要的話,那麼就切換回fully young方式,但當heap size的大小接近滿的情況下,G1會切換到partially young方式,以保證能提供足夠的記憶體空間給應用使用。

除了分代方式的劃分外,G1還支援另外一種pure G1的方式,也就是不進行代的劃分,pure方式和分代方式的具體不同在下面的具體執行步驟中進行描述。

掌握了這些概念後,繼續來看G1的具體執行步驟:

1.         Initial Marking

G1對於每個region都儲存了兩個標識用的bitmap,一個為previous marking bitmap,一個為next marking bitmap,bitmap中包含了一個bit的地址資訊來指向物件的起始點。

開始Initial Marking之前,首先併發的清空next marking bitmap,然後停止所有應用執行緒,並掃描標識出每個region中root可直接訪問到的物件,將region中top的值放入next top at mark start(TAMS)中,之後恢復所有應用執行緒。

觸發這個步驟執行的條件為:

l  G1定義了一個JVM Heap大小的百分比的閥值,稱為h,另外還有一個H,H的值為(1-h)*Heap Size,目前這個h的值是固定的,後續G1也許會將其改為動態的,根據jvm的執行情況來動態的調整,在分代方式下,G1還定義了一個u以及soft limit,soft limit的值為H-u*Heap Size,當Heap中使用的記憶體超過了soft limit值時,就會在一次clean up執行完畢後在應用允許的GC暫停時間範圍內儘快的執行此步驟;

l  在pure方式下,G1將marking與clean up組成一個環,以便clean up能充分的使用marking的資訊,當clean up開始回收時,首先回收能夠帶來最多記憶體空間的regions,當經過多次的clean up,回收到沒多少空間的regions時,G1重新初始化一個新的marking與clean up構成的環。

2.         Concurrent Marking

按照之前Initial Marking掃描到的物件進行遍歷,以識別這些物件的下層物件的活躍狀態,對於在此期間應用執行緒併發修改的物件的以來關係則記錄到remembered set logs中,新建立的物件則放入比top值更高的地址區間中,這些新建立的物件預設狀態即為活躍的,同時修改top值。

3.         Final Marking Pause

當應用執行緒的remembered set logs未滿時,是不會放入filled RS buffers中的,在這樣的情況下,這些remebered set logs中記錄的card的修改就會被更新了,因此需要這一步,這一步要做的就是把應用執行緒中存在的remembered set logs的內容進行處理,並相應的修改remembered sets,這一步需要暫停應用,並行的執行。

4.         Live Data Counting and Cleanup

值得注意的是,在G1中,並不是說Final Marking Pause執行完了,就肯定執行Cleanup這步的,由於這步需要暫停應用,G1為了能夠達到準實時的要求,需要根據使用者指定的最大的GC造成的暫停時間來合理的規劃什麼時候執行Cleanup,另外還有幾種情況也是會觸發這個步驟的執行的:

l  G1採用的是複製方法來進行收集,必須保證每次的”to space”的空間都是夠的,因此G1採取的策略是當已經使用的記憶體空間達到了H時,就執行Cleanup這個步驟;

l  對於full-young和partially-young的分代模式的G1而言,則還有情況會觸發Cleanup的執行,full-young模式下,G1根據應用可接受的暫停時間、回收young regions需要消耗的時間來估算出一個yound regions的數量值,當JVM中分配物件的young regions的數量達到此值時,Cleanup就會執行;partially-young模式下,則會盡量頻繁的在應用可接受的暫停時間範圍內執行Cleanup,並最大限度的去執行non-young regions的Cleanup。

這一步中GC執行緒並行的掃描所有region,計算每個region中低於next TAMS值中marked data的大小,然後根據應用所期望的GC的短延時以及G1對於region回收所需的耗時的預估,排序region,將其中活躍的物件複製到其他region中。

G1為了能夠儘量的做到準實時的響應,例如估算暫停時間的演算法、對於經常被引用的物件的特殊處理等,G1為了能夠讓GC既能夠充分的回收記憶體,又能夠儘量少的導致應用的暫停,可謂費盡心思,從G1的論文中的效能評測來看效果也是不錯的,不過如果G1能允許開發人員在編寫程式碼時指定哪些物件是不用mark的就更完美了,這對於有巨大快取的應用而言,會有很大的幫助,G1將隨JDK 6 Update 14 beta釋出。

= CMS ==================================

1.總體介紹:

CMS(Concurrent Mark-Sweep)是以犧牲吞吐量為代價來獲得最短回收停頓時間的垃圾回收器。併發意味著除了開頭和結束階段,需要暫停JVM,其它時間gc和應用一起執行。對於要求伺服器響應速度的應用上,這種垃圾回收器非常適合。在啟動JVM引數加上-XX:+UseConcMarkSweepGC ,這個引數表示對於老年代的回收採用CMS。CMS採用的基礎演算法是:標記—清除。預設會開啟 -XX :+UseParNewGC,在年輕代使用並行複製收集。

2.CMS過程:

  • 初始標記(STW initial mark)
  • 併發標記(Concurrent marking)
  • 併發預清理(Concurrent precleaning)
  • 重新標記(STW remark)
  • 併發清理(Concurrent sweeping)
  • 併發重置(Concurrent reset)

初始標記 :在這個階段,需要虛擬機器停頓正在執行的任務,官方的叫法STW(Stop The Word)。這個過程從垃圾回收的"根物件"開始,只掃描到能夠和"根物件"直接關聯的物件,並作標記。所以這個過程雖然暫停了整個JVM,但是很快就完成了。

併發標記 :這個階段緊隨初始標記階段,在初始標記的基礎上繼續向下追溯標記。併發標記階段,應用程式的執行緒和併發標記的執行緒併發執行,所以使用者不會感受到停頓。

併發預清理 :併發預清理階段仍然是併發的。在這個階段,虛擬機器查詢在執行併發標記階段新進入老年代的物件(可能會有一些物件從新生代晉升到老年代, 或者有一些物件被分配到老年代)。通過重新掃描,減少下一個階段"重新標記"的工作,因為下一個階段會Stop The World。

重新標記 :這個階段會暫停虛擬機器,收集器執行緒掃描在CMS堆中剩餘的物件。掃描從"跟物件"開始向下追溯,並處理物件關聯。

併發清理 :清理垃圾物件,這個階段收集器執行緒和應用程式執行緒併發執行。

併發重置 :這個階段,重置CMS收集器的資料結構,等待下一次垃圾回收。

CSM執行過程: 

3.CMS缺點

  • CMS回收器採用的基礎演算法是Mark-Sweep。所有CMS不會整理、壓縮堆空間。這樣就會有一個問題:經過CMS收集的堆會產生空間碎片。 CMS不對堆空間整理壓縮節約了垃圾回收的停頓時間,但也帶來的堆空間的浪費。為了解決堆空間浪費問題,CMS回收器不再採用簡單的指標指向一塊可用堆空 間來為下次物件分配使用。而是把一些未分配的空間彙總成一個列表,當JVM分配物件空間的時候,會搜尋這個列表找到足夠大的空間來hold住這個物件。
  • 需要更多的CPU資源。從上面的圖可以看到,為了讓應用程式不停頓,CMS執行緒和應用程式執行緒併發執行,這樣就需要有更多的CPU,單純靠執行緒切 換是不靠譜的。並且,重新標記階段,為空保證STW快速完成,也要用到更多的甚至所有的CPU資源。當然,多核多CPU也是未來的趨勢!
  • CMS的另一個缺點是它需要更大的堆空間。因為CMS標記階段應用程式的執行緒還是在執行的,那麼就會有堆空間繼續分配的情況,為了保證在CMS回 收完堆之前還有空間分配給正在執行的應用程式,必須預留一部分空間。也就是說,CMS不會在老年代滿的時候才開始收集。相反,它會嘗試更早的開始收集,已 避免上面提到的情況:在回收完成之前,堆沒有足夠空間分配!預設當老年代使用68%的時候,CMS就開始行動了。 – XX:CMSInitiatingOccupancyFraction =n 來設定這個閥值。

總得來說,CMS回收器減少了回收的停頓時間,但是降低了堆空間的利用率。

4.啥時候用CMS

如果你的應用程式對停頓比較敏感,並且在應用程式執行的時候可以提供更大的記憶體和更多的CPU(也就是硬體牛逼),那麼使用CMS來收集會給你帶來好處。還有,如果在JVM中,有相對較多存活時間較長的物件(老年代比較大)會更適合使用CMS。

= 除錯工具 ==================================

jmap

jmap -heap pid  (不能觀察G1模式)

using parallel threads in the new generation.
using thread-local object allocation.
Concurrent Mark-Sweep GC

Heap Configuration:
   MinHeapFreeRatio = 40
   MaxHeapFreeRatio = 70
   MaxHeapSize      = 2147483648 (2048.0MB)
   NewSize          = 268435456 (256.0MB)
   MaxNewSize       = 268435456 (256.0MB)
   OldSize          = 805306368 (768.0MB)
   NewRatio         = 7
   SurvivorRatio    = 8
   PermSize         = 134217728 (128.0MB)
   MaxPermSize      = 134217728 (128.0MB)

Heap Usage:
New Generation (Eden + 1 Survivor Space):
   capacity = 241631232 (230.4375MB)
   used     = 145793088 (139.03912353515625MB)
   free     = 95838144 (91.39837646484375MB)
   60.33702133340114% used
Eden Space:
   capacity = 214827008 (204.875MB)
   used     = 132689456 (126.54252624511719MB)
   free     = 82137552 (78.33247375488281MB)
   61.7657236095752% used
From Space:
   capacity = 26804224 (25.5625MB)
   used     = 13103632 (12.496597290039062MB)
   free     = 13700592 (13.065902709960938MB)
   48.886444166411984% used
To Space:
   capacity = 26804224 (25.5625MB)
   used     = 0 (0.0MB)
   free     = 26804224 (25.5625MB)
   0.0% used
concurrent mark-sweep generation: (old區)
   capacity = 1879048192 (1792.0MB)
   used     = 1360638440 (1297.6059341430664MB)
   free     = 518409752 (494.3940658569336MB)
   72.41104543209076% used
Perm Generation:
   capacity = 134217728 (128.0MB)
   used     = 65435064 (62.40373992919922MB)
   free     = 68782664 (65.59626007080078MB)
   48.75292181968689% used

jmap -histo:live pid

num     #instances         #bytes  class name
----------------------------------------------
   1:       3148147      209172848  [B
   2:       2584345      144723320  java.lang.ref.SoftReference
   3:       2578827      123783696  sun.misc.CacheEntry
   4:        781560      112544640  com.sun.net.ssl.internal.ssl.SSLSessionImpl
   5:       1385200       89970592  [C
   6:        783287       87807200  [Ljava.util.Hashtable$Entry;
   7:       1421399       56855960  java.lang.String
   8:            12       56828880  [Lsun.misc.CacheEntry;
   9:       2343358       56240592  com.sun.net.ssl.internal.ssl.SessionId
  10:        783185       50123840  java.util.Hashtable
  11:        783094       50118016  java.lang.ref.Finalizer
  12:        287243       36086720  [Ljava.lang.Object;
  13:        263376       33712128  org.apache.commons.pool.impl.GenericObjectPool

jstat

jstat -gccause 31169 60000 1000

(sweep 1,2) (Eden) (Old) (Perm) (Young GC, GCTime)(Full GC, GCTime)

  S0         S1     E         O         P     YGC     YGCT     FGC    FGCT      GCT             LGCC                 GCC                 
 48.80   0.00  68.94  69.55  48.86  30202  725.319 51835 5083.298 5808.616 unknown GCCause      No GC               
 47.98   0.00  37.47  69.61  48.86  30206  725.385 51835 5083.298 5808.682 unknown GCCause      No GC               
 50.73   0.00  51.72  69.65  48.86  30210  725.459 51835 5083.298 5808.757 unknown GCCause      No GC               
  0.00  50.02  82.67  69.60  48.84  30213  725.508 51836 5091.572 5817.081 unknown GCCause      No GC               

jstat -gcutil $pid

  S0         S1     E         O       P       YGC     YGCT    FGC    FGCT     GCT   
 74.79   0.00  95.15   0.86  37.35      2        0.112     0        0.000      0.112

O = old occupied

YGC = young gc time ( new part )

YGCT = young gc total cost time

FGC = full gc time ( old part )

FGCT = full gc total cost time

GCT = all gc cost time

jvisualvm

window下啟動遠端監控,並在被監控服務端,啟動jstatd服務。

建立安全策略檔案,並命名為jstatd.all.policy
grant codebase "file:${java.home}/../lib/tools.jar" {
    permission java.security.AllPermission;
};

jstatd -J-Djava.security.policy=jstatd.all.policy -p 8080 &


======================== Tunning ================= 典型配置:
-server -Xmx2g -Xms2g -Xmn512m -XX:PermSize=128m -Xss256k -XX:+DisableExplicitGC -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true
+AggressiveOpts 激進優化,預設開啟,使用java新特性優化 1. 預設使用序列收集器, 單個cpu時適用 2. 吞吐收集器(throughput collector):命令列引數:-XX:+UseParallelGC。在新生代使用並行清除收集策略,在舊生代和預設收集器相同。 適用:a、擁有2個以上cpu, b、臨時物件較多的程式 -XX:ParallelGCThreads 並行收集執行緒數量,最好和cpu數量相當

3. 併發收集器(concurrent low pause collector):命令列引數:-XX:+UseConcMarkSweepGC。在舊生代使用併發收集策略,大部分收集工作都是和應用併發進行的,在進行收集的時候,應用的暫停時間很短。預設配套開啟 -XX:+UseParNewGC,會在新生代使用並行複製收集。 適用:a、擁有多個cpu, b、老物件較多的程式 如果使用了UseParNewGC,那麼同時使用CMSParallelRemarkEnabled引數可以降低標識暫停 -XX:+UseCMSCompactAtFullCollection:開啟對年老代的壓縮。可能會影響效能,但是可以消除碎片
-XX:+UseFastAccessorMethods 原始型別的快速優化 -XX:SurvivorRatio 新生區中,eden&survivor的比例,設定為8 -XX:TargetSurvivorRatio 生存區需要做垃圾回收的比例值,預設為50%,設定高些可以更好的利用該區
各個垃圾收集器之間的區別: 新生代,單獨區域單獨收集,不會影響老生代,因為區域小,且允許漏收集,採用複製清除的方法,更快。

The (original) copying collector (Enabled by default). When this collector kicks in, all application threads are stopped, and the copying collection proceeds using one thread (which means only one CPU even if on a multi-CPU machine). This is known as a stop-the-world collection, because basically the JVM pauses everything else until the collection is completed.

The parallel copying collector (Enabled using -XX:+UseParNewGC). Like the original copying collector, this is a stop-the-world collector. However this collector parallelizes the copying collection over multiple threads, which is more efficient than the original single-thread copying collector for multi-CPU machines (though not for single-CPU machines). This algorithm potentially speeds up young generation collection by a factor equal to the number of CPUs available, when compared to the original singly-threaded copying collector.

The parallel scavenge collector (Enabled using -XX:UseParallelGC). This is like the previous parallel copying collector, but the algorithm is tuned for gigabyte heaps (over 10GB) on multi-CPU machines. This collection algorithm is designed to maximize throughput while minimizing pauses. It has an optional adaptive tuning policy which will automatically resize heap spaces. If you use this collector, you can only use the the original mark-sweep collector in the old generation (i.e. the newer old generation concurrent collector cannot work with this young generation collector).

UserParallelGC使用了更高效的演算法,用於處理大規模記憶體>10G場景,提供了大吞吐量功能。但是,同時在老生代,只能使用序列的標記清除方法。

老生代,必須做fullgc,必須從root開始全面標識收集。

  • The (original) mark-sweep collector (Enabled by default). This uses a stop-the-world mark-and-sweep collection algorithm. The collector is single-threaded, the entire JVM is paused and the collector uses only one CPU until completed.
  • The concurrent collector (Enabled using -XX:+UseConcMarkSweepGC). This collector tries to allow application processing to continue as much as possible during the collection. Splitting the collection into six phases described shortly, four are concurrent while two are stop-the-world:
    1. the initial-mark phase (stop-the-world, snapshot the old generation so that we can run most of the rest of the collection concurrent to the application threads);
    2. the mark phase (concurrent, mark the live objects traversing the object graph from the roots);
    3. the pre-cleaning phase (concurrent);
    4. the re-mark phase (stop-the-world, another snapshot to capture any changes to live objects since the collection started);
    5. the sweep phase (concurrent, recycles memory by clearing unreferenced objects);
    6. the reset phase (concurrent).
    If "the rate of creation" of objects is too high, and the concurrent collector is not able to keep up with the concurrent collection, it falls back to the traditional mark-sweep collector.
  • The incremental collector (Enabled using -Xincgc). The incremental collector uses a "train" algorithm to collect small portions of the old generation at a time. This collector has higher overheads than the mark-sweep collector, but because small numbers of objects are collected each time, the (stop-the-world) garbage collection pause is minimized at the cost of total garbage collection taking longer. The "train" algorithm does not guarantee a maximum pause time, but pause times are typically less than ten milliseconds.