1. 程式人生 > >JVM 基礎知識(GC)

JVM 基礎知識(GC)

幾年前寫過一篇關於JVM調優的文章,前段時間拿出來看了看,又添加了一些東西。突然發現,基礎真的很重要。學習的過程是一個由表及裡,再由裡及表的過程,所謂的“溫故而知新”。而真正能走完這個輪迴的人,也就能稱為大牛或專家了。這個過程可能來來回回,這就是所謂“螺旋上升”,而每一次輪迴都有新的發現。

這回新增的東西主要集中在基礎的一些問題上,還有一些這兩年思考的問題。這些問題可能平時我們不會刻意去想,但是真正看清楚了,卻發現還是大有裨益的,希望對大家都有幫助~

一、基礎概念

資料型別

Java虛擬機器中,資料型別可以分為兩類:基本型別和引用型別。基本型別的變數儲存原始值,即:他代表的值就是數值本身;而引用型別的變數儲存引用值。“引用值”代表了某個物件的引用,而不是物件本身,物件本身存放在這個引用值所表示的地址的位置。

基本型別包括:byte、short、int、long、char、float、double、Boolean、returnAddress

引用型別包括:類型別、介面型別、陣列

堆和棧

堆和棧是程式執行的關鍵,很有必要把他們的關係說清楚。

1)棧是執行時的單位,而堆是儲存的單位。

2)棧解決程式的執行問題,即程式如何執行,或者說如何處理資料;堆解決的是資料儲存的問題,即資料怎麼放、放在哪兒。


在Java中一個執行緒就會相應有一個執行緒棧與之對應,這點很容易理解,因為不同的執行緒執行邏輯有所不同,因此需要一個獨立的執行緒棧。而堆則是所有執行緒共享的。棧因為是執行單位,因此裡面儲存的資訊都是跟當前執行緒(或程式)相關資訊的。包括區域性變數、程式執行狀態、方法返回值等等;而堆只負責儲存物件資訊。

為什麼要把堆和棧區分出來呢?棧中不是也可以儲存資料嗎?

1從軟體設計的角度看,棧代表了處理邏輯,而堆代表了資料。這樣分開,使得處理邏輯更為清晰。分而治之的思想。這種隔離、模組化的思想在軟體設計的方方面面都有體現。

2堆與棧的分離,使得堆中的內容可以被多個棧共享(也可以理解為多個執行緒訪問同一個物件)。這種共享的收益是很多的。一方面這種共享提供了一種有效的資料互動方式(如:共享記憶體),另一方面,堆中的共享常量和快取可以被所有棧訪問,節省了空間。

3棧因為執行時的需要,比如儲存系統執行的上下文,需要進行地址段的劃分。由於棧只能向上增長,因此就會限制住棧儲存內容的能力。而堆不同,堆中的物件是可以根據需要動態增長的,因此棧和堆的拆分,使得動態增長成為可能,相應棧中只需記錄堆中的一個地址即可。

4面向物件就是堆和棧的完美結合。其實,面向物件方式的程式與以前結構化的程式在執行上沒有任何區別。但是,面向物件的引入,使得對待問題的思考方式發生了改變,而更接近於自然方式的思考。當我們把物件拆開,你會發現,物件的屬性其實就是資料,存放在堆中;而物件的行為(方法),就是執行邏輯,放在棧中。我們在編寫物件的時候,其實即編寫了資料結構,也編寫的處理資料的邏輯。不得不承認,面向物件的設計,確實很美。

在Java中,Main函式就是棧的起始點,也是程式的起始點。

程式要執行總是有一個起點的。同C語言一樣,java中的Main就是那個起點。無論什麼java程式,找到main就找到了程式執行的入口

堆中存什麼?棧中存什麼?

堆中存的是物件。棧中存的是基本資料型別堆中物件的引用。一個物件的大小是不可估計的,或者說是可以動態變化的,但是在棧中,一個物件只對應了一個4byte的引用(堆疊分離的好處)

為什麼不把基本型別放堆中呢?因為其佔用的空間一般是1~8個位元組,需要空間比較少,而且因為是基本型別,所以不會出現動態增長的情況——長度固定,因此棧中儲存就夠了,如果把他存在堆中是沒有什麼意義的(還會浪費空間,後面說明)。可以這麼說,基本型別和物件的引用都是存放在棧中,而且都是幾個位元組的一個數,因此在程式執行時,他們的處理方式是統一的。但是基本型別、物件引用和物件本身就有所區別了,因為一個是棧中的資料一個是堆中的資料。最常見的一個問題就是,Java中引數傳遞時的問題。

Java中的引數傳遞時傳值呢?還是傳引用?     

1不要試圖與C進行類比,Java中沒有指標的概念

2程式執行永遠都是在棧中進行的,因而引數傳遞時,只存在傳遞基本型別和物件引用的問題,不會直接傳物件本身。

明確以上兩點後。Java在方法呼叫傳遞引數時,因為沒有指標,所以它都是進行傳值呼叫(這點可以參考C的傳值呼叫)。因此,很多書裡面都說Java是進行傳值呼叫,這點沒有問題,而且也簡化的C中複雜性。 

但是傳引用的錯覺是如何造成的呢?在執行棧中,基本型別和引用的處理是一樣的,都是傳值,所以,如果是傳引用的方法呼叫,也同時可以理解為“傳引用值”的傳值呼叫,即引用的處理跟基本型別是完全一樣的。但是當進入被呼叫方法時,被傳遞的這個引用的值,被程式解釋(或者查詢)到堆中的物件,這個時候才對應到真正的物件。如果此時進行修改,修改的是引用對應的物件,而不是引用本身,即:修改的是堆中的資料。所以這個修改是可以保持的了。

物件,從某種意義上說,是由基本型別組成的。可以把一個物件看作為一棵樹,物件的屬性如果還是物件,則還是一顆樹(即非葉子節點),基本型別則為樹的葉子節點。程式引數傳遞時,被傳遞的值本身都是不能進行修改的,但是,如果這個值是一個非葉子節點(即一個物件引用),則可以修改這個節點下面的所有內容。

堆和棧中,棧是程式執行最根本的東西。程式執行可以沒有堆,但是不能沒有棧。而堆是為棧進行資料儲存服務,說白了堆就是一塊共享的記憶體。不過,正是因為堆和棧的分離的思想,才使得Java的垃圾回收成為可能。

Java中,棧的大小通過-Xss來設定,當棧中儲存資料比較多時,需要適當調大這個值,否則會出現java.lang.StackOverflowError異常。常見的出現這個異常的是無法返回的遞迴,因為此時棧中儲存的資訊都是方法返回的記錄點。

Java物件的大小

基本資料的型別的大小是固定的,對於非基本型別的Java物件,其大小就值得商榷。

在Java中,一個空Object物件的大小是8byte,這個大小隻是儲存堆中一個沒有任何屬性的物件的大小。看下面語句:

Object ob = new Object();

這樣在程式中完成了一個Java物件的生命,但是它所佔的空間為:4byte + 8byte

4byte,是上面部分所說的Java棧中儲存引用的所需要的空間。

8byte,是Java堆中物件的資訊。因為所有的Java非基本型別的物件都需要預設繼承Object物件,因此不論什麼樣的Java物件,其大小都必須是大於8byte。

有了Object物件的大小,我們就可以計算其他物件的大小了。

Class NewObject {

       int count;

       boolean flag;

       Object obj;

}

其大小為:空物件大小(8byte) + int大小(4byte) + Boolean大小(1byte) + 空Object引用的大小(4byte) = 17byte。但是因為Java在給物件記憶體分配時都是以8的整數倍來分,因此大於17byte的最接近8的整數倍的是24,因此此物件的大小為24byte。

這裡需要注意一下:基本型別的包裝型別的大小,因為這種包裝型別已經成為物件了,因此需要把他們作為物件來看待。包裝型別的大小至少是12byte(宣告一個空Object至少需要的空間),而且12byte沒有包含任何有效資訊,同時,因為Java物件大小是8的整數倍,因此一個基本型別包裝類的大小至少是16byte。這個記憶體佔用是很恐怖的,它是使用基本型別的N倍(N>2),有些型別的記憶體佔用更是誇張(隨便想下就知道了)。因此,可能的話應儘量少使用包裝類。在JDK5.0以後,因為加入了自動型別裝換,因此,Java虛擬機器會在儲存方面進行相應的優化。

引用型別

物件引用型別分為:強引用、軟引用、弱引用、虛引用

強引用:就是我們一般宣告物件是時虛擬機器生成的引用,強引用環境下,垃圾回收時需要嚴格判斷當前物件是否被強引用,如果被強引用,則不會被垃圾回收

軟引用軟引用一般被做為快取來使用。與強引用的區別是,軟引用在垃圾回收時,虛擬機器會根據當前系統的剩餘記憶體來決定是否對軟引用進行回收。如果剩餘記憶體比較緊張,則虛擬機器會回收軟引用所引用的空間;如果剩餘記憶體相對富裕,則不會進行回收。換句話說,虛擬機器在發生OutOfMemory時,肯定是沒有軟引用存在的。

弱引用弱引用與軟引用類似,都是作為快取來使用。但與軟引用不同,弱引用在進行垃圾回收時,是一定會被回收掉的,因此其生命週期只存在於一個垃圾回收週期內。

 強引用不用說,我們系統一般在使用時都是用的強引用。而“軟引用”和“弱引用”比較少見。他們一般被作為快取使用,而且一般是在記憶體大小比較受限的情況下做為快取。因為如果記憶體足夠大的話,可以直接使用強引用作為快取即可,同時可控性更高。因而,他們常見的是被使用在桌面應用系統的快取。


二、垃圾回收演算法

1)按照基本回收策略分

(1)引用計數(Reference Counting):

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

(2)標記-清除(Mark-Sweep)

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

(3)複製(Copying):

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

(4)標記-整理(Mark-Compact):

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

2)按分割槽對待的方式分

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

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

3)按系統執行緒分

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

(2)並行收集並行收集使用多執行緒處理垃圾回收工作,因而速度快,效率高。而且理論上CPU數目越多,越能體現出並行收集器的優勢。

(3)併發收集相對於序列收集和並行收集而言,前面兩個在進行垃圾回收工作時,需要暫停整個執行環境,而只有垃圾回收程式在執行,因此,系統在垃圾回收時會有明顯的暫停,而且暫停時間會因為堆越大而越長。

如何區分垃圾

上面說到的“引用計數”法,通過統計控制生成物件和刪除物件時的引用數來判斷。垃圾回收程式收集計數為0的物件即可。但是這種方法無法解決迴圈引用。所以,後來實現的垃圾判斷演算法中,都是從程式執行的根節點出發,遍歷整個物件引用,查詢存活的物件。那麼在這種方式的實現中,垃圾回收從哪兒開始的呢?即,從哪兒開始查詢哪些物件是正在被當前系統使用的。上面分析的堆和棧的區別,其中棧是真正進行程式執行地方,所以要獲取哪些物件正在被使用,則需要從Java棧開始。同時,一個棧是與一個執行緒對應的,因此,如果有多個執行緒的話,則必須對這些執行緒對應的所有的棧進行檢查。

同時,除了棧外,還有系統執行時的暫存器等,也是儲存程式執行資料的。這樣,以棧或暫存器中的引用為起點,我們可以找到堆中的物件,又從這些物件找到對堆中其他物件的引用,這種引用逐步擴充套件,最終以null引用或者基本型別結束,這樣就形成了一顆以Java棧中引用所對應的物件為根節點的一顆物件樹,如果棧中有多個引用,則最終會形成多顆物件樹。在這些物件樹上的物件,都是當前系統執行所需要的物件,不能被垃圾回收。而其他剩餘物件,則可以視為無法被引用到的物件,可以被當做垃圾進行回收。

因此,垃圾回收的起點是一些根物件(java棧, 靜態變數, 暫存器...)。而最簡單的Java棧就是Java程式執行的main函式。這種回收方式,也是上面提到的“標記-清除”的回收方式

如何處理碎片

由於不同Java物件存活時間是不一定的,因此,在程式執行一段時間以後,如果不進行記憶體整理,就會出現零散的記憶體碎片。碎片最直接的問題就是會導致無法分配大塊的記憶體空間,以及程式執行效率降低。所以,在上面提到的基本垃圾回收演算法中,“複製”方式和“標記-整理”方式,都可以解決碎片的問題。

如何解決同時存在的物件建立和物件回收問題

垃圾回收執行緒是回收記憶體的,而程式執行執行緒則是消耗(或分配)記憶體的,一個回收記憶體,一個分配記憶體,從這點看,兩者是矛盾的。因此,在現有的垃圾回收方式中,要進行垃圾回收前,一般都需要暫停整個應用(即:暫停記憶體的分配),然後進行垃圾回收,回收完成後再繼續應用。這種實現方式是最直接,而且最有效的解決二者矛盾的方式。

但是這種方式有一個很明顯的弊端,就是當堆空間持續增大時,垃圾回收的時間也將會相應的持續增大,對應應用暫停的時間也會相應的增大。一些對相應時間要求很高的應用,比如最大暫停時間要求是幾百毫秒,那麼當堆空間大於幾個G時,就很有可能超過這個限制,在這種情況下,垃圾回收將會成為系統執行的一個瓶頸。為解決這種矛盾,有了併發垃圾回收演算法,使用這種演算法,垃圾回收執行緒與程式執行執行緒同時執行。在這種方式下,解決了暫停的問題,但是因為需要在新生成物件的同時又要回收物件,演算法複雜性會大大增加,系統的處理能力也會相應降低,同時,“碎片”問題將會比較難解決。

為什麼要分代

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

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

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

如何分代

如上圖,虛擬機器中的共劃分為三個代:年輕代(Young Generation)、年老點(Old Generation)、持久代(Permanent Generation)

其中持久代主要存放的是Java類的類資訊,與垃圾收集要收集的Java物件關係不大。年輕代和年老代的劃分是對垃圾收集影響比較大的。

(1)年輕代:

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

(2)年老代:

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

(3)持久代:

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

什麼情況下觸發垃圾回收

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

(1)Scavenge GC

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

(2)Full GC

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

· 年老代(Tenured)被寫滿

· 持久代(Perm)被寫滿 

· System.gc()被顯示呼叫 

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

分代垃圾回收流程示意

選擇合適的垃圾收集演算法

(1)序列收集器

用單執行緒處理所有垃圾回收工作,因為無需多執行緒互動,所以效率比較高。但是,也無法使用多處理器的優勢,所以此收集器適合單處理器機器。當然,此收集器也可以用在小資料量(100M左右)情況下的多處理器機器上。可以使用-XX:+UseSerialGC開啟。

(2)並行收集器

對年輕代進行並行垃圾回收,因此可以減少垃圾回收時間。一般在多執行緒多處理器機器上使用。使用-XX:+UseParallelGC.開啟。並行收集器在J2SE5.0第六6更新上引入,在Java SE6.0中進行了增強--可以對年老代進行並行收集。如果年老代不使用併發收集的話,預設是使用單執行緒進行垃圾回收,因此會制約擴充套件能力。使用-XX:+UseParallelOldGC開啟。

使用-XX:ParallelGCThreads=<N>設定並行垃圾回收的執行緒數。此值可以設定與機器處理器數量相等。

此收集器可以進行如下配置:

最大垃圾回收暫停:指定垃圾回收時的最長暫停時間,通過-XX:MaxGCPauseMillis=<N>指定。<N>為毫秒.如果指定了此值的話,堆大小和垃圾回收相關引數會進行調整以達到指定值。設定此值可能會減少應用的吞吐量。

吞吐量吞吐量為垃圾回收時間與非垃圾回收時間的比值,通過-XX:GCTimeRatio=<N>來設定,公式為1/(1+N)。例如,-XX:GCTimeRatio=19時,表示5%的時間用於垃圾回收。預設情況為99,即1%的時間用於垃圾回收。

(3)併發收集器

可以保證大部分工作都併發進行(應用不停止),垃圾回收只暫停很少的時間,此收集器適合對響應時間要求比較高的中、大規模應用。使用-XX:+UseConcMarkSweepGC開啟。

併發收集器主要減少年老代的暫停時間,他在應用不停止的情況下使用獨立的垃圾回收執行緒,跟蹤可達物件。在每個年老代垃圾回收週期中,在收集初期併發收集器 會對整個應用進行簡短的暫停,在收集中還會再暫停一次。第二次暫停會比第一次稍長,在此過程中多個執行緒同時進行垃圾回收工作。併發收集器使用處理器換來短暫的停頓時間。在一個N個處理器的系統上,併發收集部分使用K/N個可用處理器進行回收,一般情況下1<=K<=N/4。在只有一個處理器的主機上使用併發收集器,設定為incremental mode模式也可獲得較短的停頓時間。 

浮動垃圾:由於在應用執行的同時進行垃圾回收,所以有些垃圾可能在垃圾回收進行完成時產生,這樣就造成了“Floating Garbage”,這些垃圾需要在下次垃圾回收週期時才能回收掉。所以,併發收集器一般需要20%的預留空間用於這些浮動垃圾。

 Concurrent Mode Failure:併發收集器在應用執行時進行收集,所以需要保證堆在垃圾回收的這段時間有足夠的空間供程式使用,否則,垃圾回收還未完成,堆空間先滿了。這種情況下將會發生“併發模式失敗”,此時整個應用將會暫停,進行垃圾回收。

啟動併發收集器:因為併發收集在應用執行時進行收集,所以必須保證收集完成之前有足夠的記憶體空間供程式使用,否則會出現“Concurrent Mode Failure”。通過設定-XX:CMSInitiatingOccupancyFraction=<N>指定還有多少剩餘堆時開始執行併發收集

適用場景

(1)序列處理器:

--適用情況:資料量比較小(100M左右);單處理器下並且對響應時間無要求的應用。 
--缺點:只能用於小型應用

(2)並行處理器:

--適用情況:“對吞吐量有高要求”,多CPU、對應用響應時間無要求的中、大型應用。舉例:後臺處理、科學計算。 
--缺點:垃圾收集過程中應用響應時間可能加長

(3)併發處理器:

--適用情況:“對響應時間有高要求”,多CPU、對應用響應時間有較高要求的中、大型應用。舉例:Web伺服器/應用伺服器、電信交換、整合開發環境。

小結

類別

serial collector

(單執行緒收集器)

parallel collector並行收集器)

concurrent collector
(
併發收集器)

介紹

使用單執行緒去完成所有的gc工作,沒有執行緒間的通訊,這種方式會相對高效

使用多執行緒的方式,利用多CUP來提高GC的效率,主要以到達一定的吞吐量為目標

使用多執行緒的方式,利用多CUP來提高GC的效率,併發完成大部分工作,使得gc pause

適用場景

單處理器機器且沒有pause time的要求

適用於科學技術和後臺處理有中規模/大規模資料集大小的應用且執行在多處理器上,關注吞吐量(throughput)

適合中大規模資料集的應用,應用伺服器,電信領域,關注response time,而不是throughput

使用引數

Client模式下預設:

可使用

強制使用引數:

-XX:+UseSerialGC

優點:對server應用沒什麼優點缺點:慢,不能充分發揮硬體資源

Server模式下預設

--YGC:PS FGC:Parallel MSC

強制使用引數:

-XX:+UseParallelGC-XX:+UseParallelOldGC

--ParallelGC代表FGCParallel MSC

--ParallelOldGC代表FGCParallel Compacting

優點:高效

缺點heap變大後造成暫停時間會變長

可用-XX:+UseConcMarkSweepGC強制指定

優點:
對old進行回收時,對應用造成的暫停時間非常短,適合對latency要求比較高的應用
缺點:
1.記憶體碎片和浮動垃圾
2.old去的記憶體分配效率低
3.回收的整個耗時比較長
4.和應用爭搶CPU

記憶體回收觸發條件

YGC
eden空間不足


FGC
old
空間不足
perm
空間不足顯示呼叫System.gc(),包括RMI等的定時觸發

YGC
eden空間不足


FGC
old
空間不足
perm
空間不足顯示呼叫System.gc(),包括RMI等的定時觸發

YGC
eden空間不足


CMS GC
1.old Gen
使用率大的比率,預設為92%
2.
配置了CMSClassUnloadingEnabled,Perm Gen的使用達到一定的比率預設為92%
3.Hotspot
自己根據估計決定是否要觸法
4.
在配置了ExplictGCInvokesConcurrent的情況下顯示呼叫了System.gc()

記憶體回收觸發工作

YGC
清空eden+from中所有no ref的物件佔用的記憶體eden+from中的所有存活的物件copyto在這個過程中一些物件將晉升到old:
--to
放不下的
--
存活次數超過tenuring threshold重新計算Tenuring Threshold;
單執行緒做以上動作,GC全程暫停應用
FGC
如果配置了CollectGen0First,則先觸發YGC
清空heapno ref的物件,permgen中已經被解除安裝的classloader中載入的class的資訊單執行緒做以上動作全程暫停應用

YGC
serial動作基本相同,不同點:
1.
多執行緒處理
2.YGC
的最後不僅重新計算Tenuring Threshold,還會重新調整EdenFrom的大小
FGC
1.
如配置了ScavengeBeforeFullGC(預設),則先觸發YGC(??)
2.MSC:
清空heap中的no ref物件,permgen中已經被解除安裝的classloader中載入的class資訊,並進行壓縮
3.Compacting:
清空heap中部分no ref的物件,permgen中已經被解除安裝的classloader中載入的class資訊,並進行部分壓縮多執行緒做以上動作.

YGCserial動作基本相同,不同點:
1.
多執行緒處理
CMSGC:
1.old gen
到達比率時只清除old genno ref的物件所佔用的空間
2.perm gen
到達比率時只清除已被清除的classloader載入的class資訊
FGC
serial

細節引數

可用-XX:+UseSerialGC強制使用


-XX:SurvivorRatio=x,
控制eden/s0/s1的大小
-XX:MaxTenuringThreshold,
用於控制物件在新生代存活的最大次數


-XX:PretenureSizeThreshold=x,
控制超過多大的位元組的物件就在old分配.

-XX:SurvivorRatio=x,控制eden/s0/s1的大小
-XX:MaxTenuringThreshold,
用於控制物件在新生代存活的最大次數

-XX:UseAdaptiveSizePolicy 去掉YGC後動態調整eden from已經tenuringthreshold的動作

-XX:ParallelGCThreads=4設定並行的執行緒數

-XX:CMSInitiatingOccupancyFraction 設定old gen使用到達多少比率時觸發
-XX:CMSInitiatingPermOccupancyFraction,
設定Perm Gen使用到達多少比率時觸發
-XX:+UseCMSInitiatingOccupancyOnly
禁止hostspot自行觸發CMS GC

三、典型配置舉例

以下配置主要針對分代垃圾回收演算法而言。

堆大小設定

年輕代的設定很關鍵

JVM中最大堆大小有三方面限制:相關作業系統的資料模型(32-bt還是64-bit)限制;系統的可用虛擬記憶體限制;系統的可用實體記憶體限制。32位系統下,一般限制在1.5G~2G;64為作業系統對記憶體無限制。在Windows Server 2003 系統,3.5G實體記憶體,JDK5.0下測試,最大可設定為1478m。

典型設定:

java -Xmx3550m -Xms3550m -Xmn2g –Xss128k

-Xmx3550m:設定JVM最大可用記憶體為3550M。

-Xms3550m:設定JVM促使記憶體為3550m。此值可以設定與-Xmx相同,以避免每次垃圾回收完成後JVM重新分配記憶體。

-Xmn2g:設定年輕代大小為2G。整個堆大小=年輕代大小 + 年老代大小 + 持久代大小。持久代一般固定大小為64m,所以增大年輕代後,將會減小年老代大小。此值對系統性能影響較大,Sun官方推薦配置為整個堆的3/8。

-Xss128k:設定每個執行緒的堆疊大小。JDK5.0以後每個執行緒堆疊大小為1M,以前每個執行緒堆疊大小為256K。更具應用的執行緒所需記憶體大小進行調整。在相同實體記憶體下,減小這個值能生成更多的執行