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

一、基礎概念

數據類型

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信息,並進行部分壓縮
多線程做以上動作.

YGC
serial動作基本相同,不同點:
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。更具應用的線程所需內存大小進行調整。在相同物理內存下,減小這個值能生成更多的線程。但是操作系統對一個進程內的線程數還是有限制的,不能無限生成,經驗值在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 算法詳解

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算法進行調優了。

發佈了55 篇原創文章 · 獲贊 34 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章