天天看點

關于JVM的GC總結(涉及GC算法)

一、基礎概念

資料類型

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

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

引用類型包括:類類型、接口類型、數組

堆和棧

堆和棧是程式運作的關鍵,很有必要把他們的關系說清楚。

1)棧是運作時的機關,而堆是存儲的機關。

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

關于JVM的GC總結(涉及GC算法)

在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):

關于JVM的GC總結(涉及GC算法)

此算法執行分兩階段。第一階段從引用根節點開始标記所有被引用的對象,第二階段周遊整個堆,把未标記的對象清除。此算法需要暫停整個應用,同時,會産生記憶體碎片。

(3)複制(Copying):

關于JVM的GC總結(涉及GC算法)

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

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

關于JVM的GC總結(涉及GC算法)

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

2)按分區對待的方式分

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

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

3)按系統線程分

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

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

(3)并發收集:相對于串行收集和并行收集而言,前面兩個在進行垃圾回收工作時,需要暫停整個運作環境,而隻有垃圾回收程式在運作,是以,系統在垃圾回收時會有明顯的暫停,而且暫停時間會因為堆越大而越長。

如何區分垃圾

上面說到的“引用計數”法,通過統計控制生成對象和删除對象時的引用數來判斷。垃圾回收程式收集計數為0的對象即可。但是這種方法無法解決循環引用。是以,後來實作的垃圾判斷算法中,都是從程式運作的根節點出發,周遊整個對象引用,查找存活的對象。那麼在這種方式的實作中,垃圾回收從哪兒開始的呢?即,從哪兒開始查找哪些對象是正在被目前系統使用的。上面分析的堆和棧的差別,其中棧是真正進行程式執行地方,是以要擷取哪些對象正在被使用,則需要從Java棧開始。同時,一個棧是與一個線程對應的,是以,如果有多個線程的話,則必須對這些線程對應的所有的棧進行檢查。

關于JVM的GC總結(涉及GC算法)

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

是以,垃圾回收的起點是一些根對象(java棧, 靜态變量, 寄存器...)。而最簡單的Java棧就是Java程式執行的main函數。這種回收方式,也是上面提到的“标記-清除”的回收方式

如何處理碎片

由于不同Java對象存活時間是不一定的,是以,在程式運作一段時間以後,如果不進行記憶體整理,就會出現零散的記憶體碎片。碎片最直接的問題就是會導緻無法配置設定大塊的記憶體空間,以及程式運作效率降低。是以,在上面提到的基本垃圾回收算法中,“複制”方式和“标記-整理”方式,都可以解決碎片的問題。

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

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

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

為什麼要分代

分代的垃圾回收政策,是基于這樣一個事實:不同的對象的生命周期是不一樣的。是以,不同生命周期的對象可以采取不同的收集方式,以便提高回收效率。

在Java程式運作的過程中,會産生大量的對象,其中有些對象是與業務資訊相關,比如Http請求中的Session對象、線程、Socket連接配接,這類對象跟業務直接挂鈎,是以生命周期比較長。但是還有一些對象,主要是程式運作過程中生成的臨時變量,這些對象生命周期會比較短,比如:String對象,由于其不變類的特性,系統會産生大量的這些對象,有些對象甚至隻用一次即可回收。

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

如何分代

關于JVM的GC總結(涉及GC算法)

如上圖,虛拟機中的共劃分為三個代:年輕代(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的各域配置設定政策動态變化

分代垃圾回收流程示意

關于JVM的GC總結(涉及GC算法)
關于JVM的GC總結(涉及GC算法)
關于JVM的GC總結(涉及GC算法)
關于JVM的GC總結(涉及GC算法)

選擇合适的垃圾收集算法

(1)串行收集器

關于JVM的GC總結(涉及GC算法)

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

(2)并行收集器

關于JVM的GC總結(涉及GC算法)

對年輕代進行并行垃圾回收,是以可以減少垃圾回收時間。一般在多線程多處理器機器上使用。使用-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打開。

關于JVM的GC總結(涉及GC算法)

并發收集器主要減少年老代的暫停時間,他在應用不停止的情況下使用獨立的垃圾回收線程,跟蹤可達對象。在每個年老代垃圾回收周期中,在收集初期并發收集器 會對整個應用進行簡短的暫停,在收集中還會再暫停一次。第二次暫停會比第一次稍長,在此過程中多個線程同時進行垃圾回收工作。并發收集器使用處理器換來短暫的停頓時間。在一個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代表FGC為Parallel MSC

--ParallelOldGC代表FGC為Parallel 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中的所有存活的對象copy到to中

在這個過程中一些對象将晉升到old中:

--to放不下的

--存活次數超過tenuring threshold的

重新計算Tenuring Threshold;

單線程做以上動作,GC全程暫停應用

FGC

如果配置了CollectGen0First,則先觸發YGC

清空heap中no ref的對象,permgen中已經被解除安裝的classloader中加載的class的資訊

單線程做以上動作

全程暫停應用

YGC

同serial動作基本相同,不同點:

1.多線程處理

2.YGC的最後不僅重新計算Tenuring Threshold,還會重新調整Eden和From的大小

FGC

1.如配置了ScavengeBeforeFullGC(預設),則先觸發YGC(??)

2.MSC:清空heap中的no ref對象,permgen中已經被解除安裝的classloader中加載的class資訊,并進行壓縮

3.Compacting:清空heap中部分no ref的對象,permgen中已經被解除安裝的classloader中加載的class資訊,并進行部分壓縮

多線程做以上動作.

YGC

同serial動作基本相同,不同點:

1.多線程處理

CMSGC:

1.old gen到達比率時隻清除old gen中no 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。更具應用的線程所需記憶體大小進行調整。在相同實體記憶體下,減小這個值能生成更多的線程。但是作業系統對一個程序内的線程數還是有限制的,不能無限生成,經驗值在3000~5000左右。

java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0

-XX:NewRatio=4:設定年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代)。設定為4,則年輕代與年老代所占比值為1:4,年輕代占整個堆棧的1/5

-XX:SurvivorRatio=4:設定年輕代中Eden區與Survivor區的大小比值。設定為4,則兩個Survivor區與一個Eden區的比值為2:4,一個Survivor區占整個年輕代的1/6

-XX:MaxPermSize=16m:設定持久代大小為16m。

-XX:MaxTenuringThreshold=0:設定垃圾最大年齡。如果設定為0的話,則年輕代對象不經過Survivor區,直接進入年老代。對于年老代比較多的應用,可以提高效率。如果将此值設定為一個較大值,則年輕代對象會在Survivor區進行多次複制,這樣可以增加對象再年輕代的存活時間,增加在年輕代即被回收的概論。

回收器選擇

JVM給了三種選擇:串行收集器、并行收集器、并發收集器

(1)串行收集器

适用于小資料量的情況,是以這裡的選擇主要針對并行收集器和并發收集器。預設情況下,JDK5.0以前都是使用串行收集器,如果想使用其他收集器需要在啟動時加入相應參數。JDK5.0以後,JVM會根據目前系統配置進行判斷。

(2)并行收集器——吞吐量優先

如上文所述,并行收集器主要以到達一定的吞吐量為目标,适用于科學技術和背景處理等。

典型配置:

java -Xmx3800m -Xms3800m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20

-XX:+UseParallelGC:選擇垃圾收集器為并行收集器。此配置僅對年輕代有效。即上述配置下,年輕代使用并發收集,而年老代仍舊使用串行收集。

-XX:ParallelGCThreads=20:配置并行收集器的線程數,即:同時多少個線程一起進行垃圾回收。此值最好配置與處理器數目相等。

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC

-XX:+UseParallelOldGC:配置年老代垃圾收集方式為并行收集。JDK6.0支援對年老代并行收集。

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100

-XX:MaxGCPauseMillis=100:設定每次年輕代垃圾回收的最長時間,如果無法滿足此時間,JVM會自動調整年輕代大小,以滿足此值。

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseParallelGC  -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy

-XX:+UseAdaptiveSizePolicy:設定此選項後,并行收集器會自動選擇年輕代區大小和相應的Survivor區比例,以達到目标系統規定的最低相應時間或者收集頻率等,此值建議使用并行收集器時,一直打開。

(3)并發收集器——響應時間優先

如上文所述,并發收集器主要是保證系統的響應時間,減少垃圾收集時的停頓時間。适用于應用伺服器、電信領域等。

典型配置:

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC

-XX:+UseConcMarkSweepGC:設定年老代為并發收集。測試中配置這個以後,-XX:NewRatio=4的配置失效了,原因不明。是以,此時年輕代大小最好用-Xmn設定。

-XX:+UseParNewGC: 設定年輕代為并行收集。可與CMS收集同時使用。JDK5.0以上,JVM會根據系統配置自行設定,是以無需再設定此值。

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:+UseConcMarkSweepGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection

-XX:CMSFullGCsBeforeCompaction:由于并發收集器不對記憶體空間進行壓縮、整理,是以運作一段時間以後會産生“碎片”,使得運作效率降低。此值設定運作多少次GC以後對記憶體空間進行壓縮、整理。

-XX:+UseCMSCompactAtFullCollection:打開對年老代的壓縮。可能會影響性能,但是可以消除碎片

輔助資訊

JVM提供了大量指令行參數,列印資訊,供調試使用。主要有以下一些:

-XX:+PrintGC:輸出形式:[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]

-XX:+PrintGCDetails:輸出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]

-XX:+PrintGCTimeStamps -XX:+PrintGC:PrintGCTimeStamps可與上面兩個混合使用 

輸出形式:11.851: [GC 98328K->93620K(130112K), 0.0082960 secs]

-XX:+PrintGCApplicationConcurrentTime:列印每次垃圾回收前,程式未中斷的執行時間。可與上面混合使用。輸出形式:Application time: 0.5291524 seconds

-XX:+PrintGCApplicationStoppedTime:列印垃圾回收期間程式暫停的時間。可與上面混合使用。輸出形式:Total time for which application threads were stopped: 0.0468229 seconds

-XX:PrintHeapAtGC: 列印GC前後的詳細堆棧資訊。輸出形式:

34.702: [GC {Heap before gc invocations=7:

def new generation   total 55296K, used 52568K [0x1ebd0000, 0x227d0000, 0x227d0000)

eden space 49152K,  99% used [0x1ebd0000, 0x21bce430, 0x21bd0000)

from space 6144K,  55% used [0x221d0000, 0x22527e10, 0x227d0000)

to   space 6144K,   0% used [0x21bd0000, 0x21bd0000, 0x221d0000)

tenured generation   total 69632K, used 2696K [0x227d0000, 0x26bd0000, 0x26bd0000)

the space 69632K,   3% used [0x227d0000, 0x22a720f8, 0x22a72200, 0x26bd0000)

compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)

   the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)

ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)

rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)

34.735: [DefNew: 52568K->3433K(55296K), 0.0072126 secs] 55264K->6615K(124928K)Heap after gc invocations=8:

def new generation   total 55296K, used 3433K [0x1ebd0000, 0x227d0000, 0x227d0000)

eden space 49152K,   0% used [0x1ebd0000, 0x1ebd0000, 0x21bd0000)

  from space 6144K,  55% used [0x21bd0000, 0x21f2a5e8, 0x221d0000)

  to   space 6144K,   0% used [0x221d0000, 0x221d0000, 0x227d0000)

tenured generation   total 69632K, used 3182K [0x227d0000, 0x26bd0000, 0x26bd0000)

the space 69632K,   4% used [0x227d0000, 0x22aeb958, 0x22aeba00, 0x26bd0000)

compacting perm gen  total 8192K, used 2898K [0x26bd0000, 0x273d0000, 0x2abd0000)

   the space 8192K,  35% used [0x26bd0000, 0x26ea4ba8, 0x26ea4c00, 0x273d0000)

   ro space 8192K,  66% used [0x2abd0000, 0x2b12bcc0, 0x2b12be00, 0x2b3d0000)

   rw space 12288K,  46% used [0x2b3d0000, 0x2b972060, 0x2b972200, 0x2bfd0000)

}

, 0.0757599 secs]

-Xloggc:filename:與上面幾個配合使用,把相關日志資訊記錄到檔案以便分析。

四、常見配置彙總

堆設定

  -Xms:初始堆大小

  -Xmx:最大堆大小

  -XX:NewSize=n:設定年輕代大小

  -XX:NewRatio=n:設定年輕代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代占整個年輕代年老代和的1/4

  -XX:SurvivorRatio=n:年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區占整個年輕代的1/5

  -XX:MaxPermSize=n:設定持久代大小

收集器設定

  -XX:+UseSerialGC:設定串行收集器

  -XX:+UseParallelGC:設定并行收集器

  -XX:+UseParalledlOldGC:設定并行年老代收集器

  -XX:+UseConcMarkSweepGC:設定并發收集器

垃圾回收統計資訊

  -XX:+PrintGC

  -XX:+PrintGCDetails

  -XX:+PrintGCTimeStamps

  -Xloggc:filename

并行收集器設定

  -XX:ParallelGCThreads=n:設定并行收集器收集時使用的CPU數。并行收集線程數。

  -XX:MaxGCPauseMillis=n:設定并行收集最大暫停時間

  -XX:GCTimeRatio=n:設定垃圾回收時間占程式運作時間的百分比。公式為1/(1+n)

并發收集器設定

  -XX:+CMSIncrementalMode:設定為增量模式。适用于單CPU情況。

  -XX:ParallelGCThreads=n:設定并發收集器年輕代收集方式為并行收集時,使用的CPU數。并行收集線程數。

調優總結

年輕代大小選擇

響應時間優先的應用:盡可能設大,直到接近系統的最低響應時間限制(根據實際情況選擇)。在此種情況下,年輕代收集發生的頻率也是最小的。同時,減少到達年老代的對象。

吞吐量優先的應用:盡可能的設定大,可能到達Gbit的程度。因為對響應時間沒有要求,垃圾收集可以并行進行,一般适合8CPU以上的應用。

年老代大小選擇

響應時間優先的應用:年老代使用并發收集器,是以其大小需要小心設定,一般要考慮并發會話率和會話持續時間等一些參數。如果堆設定小了,可以會造成記憶體碎片、高回收頻率以及應用暫停而使用傳統的标記清除方式;如果堆大了,則需要較長的收集時間。最優化的方案,一般需要參考以下資料獲得:

  1. 并發垃圾收集資訊

  2. 持久代并發收集次數

  3. 傳統GC資訊

  4. 花在年輕代和年老代回收上的時間比例

減少年輕代和年老代花費的時間,一般會提高應用的效率

吞吐量優先的應用:一般吞吐量優先的應用都有一個很大的年輕代和一個較小的年老代。原因是,這樣可以盡可能回收掉大部分短期對象,減少中期的對象,而年老代盡存放長期存活對象。

較小堆引起的碎片問題

因為年老代的并發收集器使用标記、清除算法,是以不會對堆進行壓縮。當收集器回收時,他會把相鄰的空間進行合并,這樣可以配置設定給較大的對象。但是,當堆空間較小時,運作一段時間以後,就會出現“碎片”,如果并發收集器找不到足夠的空間,那麼并發收集器将會停止,然後使用傳統的标記、清除方式進行回收。如果出現“碎片”,可能需要進行如下配置:

1. -XX:+UseCMSCompactAtFullCollection:使用并發收集器時,開啟對年老代的壓縮。

2. -XX:CMSFullGCsBeforeCompaction=0:上面配置開啟的情況下,這裡設定多少次Full GC後,對年老代進行壓縮

垃圾回收的瓶頸

傳統分代垃圾回收方式,已經在一定程度上把垃圾回收給應用帶來的負擔降到了最小,把應用的吞吐量推到了一個極限。但是他無法解決的一個問題,就是Full GC所帶來的應用暫停。在一些對實時性要求很高的應用場景下,GC暫停所帶來的請求堆積和請求失敗是無法接受的。這類應用可能要求請求的傳回時間在幾百甚至幾十毫秒以内,如果分代垃圾回收方式要達到這個名額,隻能把最大堆的設定限制在一個相對較小範圍内,但是這樣有限制了應用本身的處理能力,同樣也是不可接收的。

    分代垃圾回收方式确實也考慮了實時性要求而提供了并發回收器,支援最大暫停時間的設定,但是受限于分代垃圾回收的記憶體劃分模型,其效果也不是很理想。

    為了達到實時性的要求(其實Java語言最初的設計也是在嵌入式系統上的),一種新垃圾回收方式呼之欲出,它既支援短的暫停時間,又支援大的記憶體空間配置設定。可以很好的解決傳統分代方式帶來的問題。

增量收集的演進

    增量收集的方式在理論上可以解決傳統分代方式帶來的問題。增量收集把對堆空間劃分成一系列記憶體塊,使用時,先使用其中一部分(不會全部用完),垃圾收集時把之前用掉的部分中的存活對象再放到後面沒有用的空間中,這樣可以實作一直邊使用邊收集的效果,避免了傳統分代方式整個使用完了再暫停的回收的情況。

    當然,傳統分代收集方式也提供了并發收集,但是他有一個很緻命的地方,就是把整個堆做為一個記憶體塊,這樣一方面會造成碎片(無法壓縮),另一方面他的每次收集都是對整個堆的收集,無法進行選擇,在暫停時間的控制上還是很弱。而增量方式,通過記憶體空間的分塊,恰恰可以解決上面問題。

Garbage Firest(G1)

這部分的内容主要參考這裡,這篇文章算是對G1算法論文的解讀。我也沒加什麼東西了。

目标:從設計目标看G1完全是為了大型應用而準備的。

支援很大的堆

高吞吐量

  --支援多CPU和垃圾回收線程

  --在主線程暫停的情況下,使用并行收集

  --在主線程運作的情況下,使用并發收集

實時目标:可配置在N毫秒内最多隻占用M毫秒的時間進行垃圾回收

當然G1要達到實時性的要求,相對傳統的分代回收算法,在性能上會有一些損失。

G1 算法詳解

關于JVM的GC總結(涉及GC算法)

G1可謂博采衆家之長,力求到達一種完美。他吸取了增量收集優點,把整個堆劃分為一個一個等大小的區域(region)。記憶體的回收和劃分都以region為機關;同時,他也吸取了CMS的特點,把這個垃圾回收過程分為幾個階段,分散一個垃圾回收過程;而且,G1也認同分代垃圾回收的思想,認為不同對象的生命周期不同,可以采取不同收集方式,是以,它也支援分代的垃圾回收。為了達到對回收時間的可預計性,G1在掃描了region以後,對其中的活躍對象的大小進行排序,首先會收集那些活躍對象小的region,以便快速回收空間(要複制的活躍對象少了),因為活躍對象小,裡面可以認為多數都是垃圾,是以這種方式被稱為Garbage First(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)中,之後恢複所有應用線程。觸發這個步驟執行的條件為:

    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暫停時間範圍内盡快的執行此步驟;

    在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,另外還有幾種情況也是會觸發這個步驟的執行的:

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

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

以後JVM的調優或許跟多需要針對G1算法進行調優了。

繼續閱讀