1.什麼情況下發生GC

一:jvm運行時數據區的劃分

        (1)程序計數器(Program Conuter Register)

        程序計數器是一塊較小的內存空間,它是當前線程執行字節碼的行號指示器,字節碼解釋工作器就是通過改變這個計數器的值來選取下一條需要執行的指令。它是線程私有的內存,也是唯一一個沒有OOM異常的區域。

        (2)Java虛擬機棧區(Java Virtual Machine Stacks)

        棧區是Java方法執行的內存模型,每個方法被執行的時候都創建一個棧幀(Stack Frame),用於存儲局部變量表、操作數棧、動態鏈接、方法出口等。每個方法被調用到完成,相當於一個棧幀在虛擬機棧中從入棧到出棧的過程。此區域也是線程私有的內存,可能拋出兩種異常:如果線程請求的棧深度大於虛擬機允許的深度將拋出StackOverflowError;如果虛擬機棧可以動態的擴展,擴展到無法動態的申請到足夠的內存時會拋出OOM異常。

        (3)本地方法棧(Native Method Stacks)

        本地方法棧與虛擬機棧發揮的作用相似,區別就是虛擬機棧爲虛擬機執行Java方法,本地方法棧則是爲虛擬機使用到的Native方法服務。

        (4)堆區(Heap)

        所有對象實例和數組都在堆區上分配,堆區是GC主要管理的區域。堆區還可以細分爲新生代、老年代,新生代還分爲一個Eden區和兩個Survivor區。此塊內存爲所有線程共享區域,當堆中沒有足夠內存完成實例分配時會拋出OOM異常。

        (5)方法區(Method Area)

       方法區也是所有線程共享區,用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯後的代碼等數據。GC在這個區域很少出現,這個區域內存回收的目標主要是對常量池的回收和類型的卸載,回收的內存比較少,所以也有稱這個區域爲永久代(Permanent Generation)的。當方法區無法滿足內存分配時拋出OOM異常。

        (6)運行時常量池(Runtime Constant Pool)

        運行時常量池是方法區的一部分,用於存放編譯期生成的各種字面量和符號引用。

二:堆區的劃分(where)

堆內存劃分爲 Eden、Survivor 和 Tenured/Old 空間,如下圖所示:

       從年輕代空間(包括 Eden 和 Survivor 區域)回收內存被稱爲 Minor GC,對老年代GC稱爲Major GC,而Full GC是對整個堆來說的,在最近幾個版本的JDK裏默認包括了對永生代,即方法區的回收(JDK8中無永生代),出現Full GC的時候經常伴隨至少一次的Minor GC,但非絕對的。Major GC的速度一般會比Minor GC慢10倍以上。

三:什麼情況下發生GC(when)

(1)Minor GC觸發的條件

      1)新生代中Eden空間不足,對象優先在Eden中分配,當Eden中沒有足夠空間時,虛擬機將發生一次Minor GC,因爲Java大多數對象都是朝生夕滅,所以Minor GC非常頻繁,而且速度也很快;

      2)發生Minor GC時,虛擬機會檢測之前每次晉升到老年代的平均大小是否大於老年代的剩餘空間大小,如果大於,則進行一次Full GC,如果小於,則查看HandlePromotionFailure設置是否允許擔保失敗,如果允許,那隻會進行一次Minor GC,如果不允許,則改爲進行一次Full GC。

(2)Full GC觸發的條件

       1)System.gc()方法的調用,此方法是建議JVM進行Full GC,雖然只是建議而非一定,但很多情況下它會觸發 Full GC,從而增加Full GC的頻率,也即增加了間歇性停頓的次數。一般情況下不使用此方法,讓虛擬機自己去管理它的內存,可通過-XX:+ DisableExplicitGC來禁止RMI調用System.gc()。 

      2) 老年代的內存空間不足,發生Full GC一般都會有一次Minor GC。大對象直接進入老年代,如很長的字符串數組,虛擬機提供一個-XX:PretenureSizeThreadhold參數,令大於這個參數值的對象直接在老年代中分配,避免在Eden區和兩個Survivor區發生大量的內存拷貝;

     3)方法區內存空間不足,Permanet Generation中存放的爲一些class的信息、常量、靜態變量等數據,當系統中要加載的類、反射的類和調用的方法較多時,Permanet Generation可能會被佔滿,在未配置爲採用CMS GC的情況下也會執行Full GC。如果經過Full GC仍然回收不了,那麼JVM會拋出如下錯誤信息:java.lang.OutOfMemoryError: PermGen space
爲避免Perm Gen佔滿造成Full GC現象,可採用的方法爲增大Perm Gen空間或轉爲使用CMS GC。

    4)當Minor GC時,老年代的剩餘空間小於歷次從新生代往老年代中移的對象的平均內存空間大小時,Hotspot爲了避免由於新生代對象晉升到舊生代導致舊生代空間不足的現象,在進行Minor GC時,做了一個判斷,如果之前統計所得到的Minor GC晉升到舊生代的平均大小大於舊生代的剩餘空間,那麼就直接觸發Full GC。
      例如程序第一次觸發Minor GC後,有6MB的對象晉升到舊生代,那麼當下一次Minor GC發生時,首先檢查舊生代的剩餘空間是否大於6MB,如果小於6MB,則執行Full GC。
      當新生代採用PS GC時,方式稍有不同,PS GC是在Minor GC後也會檢查,例如上面的例子中第一次Minor GC後,PS GC會檢查此時舊生代的剩餘空間是否大於6MB,如小於,則觸發對舊生代的回收。
     對於使用RMI來進行RPC或管理的Sun JDK應用而言,默認情況下會一小時執行一次Full GC。可在啓動時通過- java -

Dsun.rmi.dgc.client.gcInterval=3600000來設置Full GC執行的間隔時間或通過-XX:+ DisableExplicitGC來禁止RMI調用System.gc。

    5)堆中分配很大的對象,大對象是指需要大量連續內存空間的java對象,例如很長的數組,此種對象會直接進入老年代,而老年代雖然有很大的剩餘空間,但是無法找到足夠大的連續空間來分配給當前對象,此種情況就會觸發JVM進行Full GC。爲了解決這個問題,CMS垃圾收集器提供了一個可配置的參數,即-XX:+UseCMSCompactAtFullCollection開關參數,用於在“享受”完Full GC服務之後額外免費贈送一個碎片整理的過程,內存整理的過程無法併發的,空間碎片問題沒有了,但提頓時間不得不變長了,JVM設計者們還提供了另外一個參數 -XX:CMSFullGCsBeforeCompaction,這個參數用於設置在執行多少次不壓縮的Full GC後,跟着來一次帶壓縮的。

四:哪些內存需要回收(垃圾對象who)

     (1)引用計數法,每個對象創建的時候,會分配一個引用計數器,當這個對象被引用的時候計數器就加1,當不被引用或者引用失效的時候計數器就會減1。任何時候,對象的引用計數器值爲0就說明這個對象不被使用了,就認爲是“垃圾”,可以被GC處理掉。

       優點:算法實現簡單。

       缺點:不能解決對象之間循環引用的問題。有垃圾對象不能被正確識別,這對垃圾回收來說是很致命的,所以GC並沒有使用這種搜索算法。

(2)根搜索算法,以一些特定的對象作爲基礎原始對象,或者稱作“根”,不斷往下搜索,到達某一個對象的路徑稱爲引用鏈。如果一個對象和根對象之間有引用鏈,即根對象到這個對象是可到達的,則這個對象是活着的,不是垃圾,還不能回收。例如,假設有根對象O,O引用了A對象,同時A對象引用了B對象,B對象又引用了C對象,那麼對象C和根對象O之間的路徑的可達的,C對象就不能當做垃圾對象。引用鏈爲O->A->B->C。反之,如果一個對象和根對象之間沒有引用鏈,根對象到這個對象的路徑是不可達的,那麼這個對象就是可回收的垃圾對象。

        從gc root開始搜索找不到的對象,而且經過一次標記、清理,仍然沒有復活的對象,當一個對象到GC Roots沒有任何引用相連接,用圖論的來說就是從GC Roots到這個對象不可達,則證明此對象是不可用的,說明此對象可以被GC。對於這些不可達對象,也不是一下子就被GC,而是至少要經歷兩次標記過程:如果對象在進行根搜索算法(GC Root Tracing)後發現沒有與GC Roots相連接的引用鏈,那它將會第一次標記並且進行一次篩選,篩選條件是此對象有沒有必要執行finalize()方法,當對象沒有覆蓋finalize()方法或者finalize()方法已經被虛擬機調用執行過一次,這兩種情況都被視爲沒有必要執行finalize()方法,對於沒有必要執行finalize()方法的將會被GC,對於有必要有必要執行的,對象在finalize()方法中可能會自救,也就是重新與引用鏈上的任何一個對象建立關聯即可。

        優點:可找到所以得垃圾對象,並且完美解決對象之間循環引用的問題。

        缺點:不可避免地要遍歷全局所有對象,導致搜索效率不高。

根搜索算法是現在GC使用的搜索算法。

可以當做GC roots的對象有以下幾種:

       1)虛擬機棧中的引用的對象。(java棧的棧幀本地變量表)

       2)方法區中的類靜態屬性引用的對象。

      3)方法區中的常量引用的對象。(聲明爲final的常量對象)

      4)本地方法棧中JNI的引用的對象。(本地方法棧的棧幀本地變量表)

 

       下圖可以看到:GC ROOTS就是根對象節點,藍色的是可達的引用鏈,引用鏈上的對象是活着的,不能被當做垃圾對象回收。相反暗灰色的路徑表示不可達的路徑,這些對象將會被回收。每個圈圈裏面的數字,表示其被引用的次數,就是上面說到的引用計數法的計數值。

 

跟搜索算法示例圖

當對象被判定爲不可達對象後,是不是就一定非清除不可呢

    答案:不是,不可達對象可以通過覆寫finalize()方法,來自救。

判定一個類是否可以被移除,需要滿足下面幾個條件:

  • 該類所有的實例都已被回收

  • 加載該類的ClassLoader已被回收

  • 該類的Class對象沒有被任何地方引用,無法在任何地方通過反射訪問該類的方法。

滿足上述條件,表明這個類可以被回收,而不是必然會被回收。是否回收,Hotspot使用-Xnoclassgc控制。

五:GC如何回收(how)

       選擇不同的垃圾收集器,所使用的收集算法也不同。

       在新生代中,每次垃圾收集都發現有大批對象死去,只有少量存活,則使用複製算法,新生代內存被分爲一個較大的Eden區和兩個較小的Survivor區,每次只使用Eden區和一個Survivor區,當回收時將Eden區和Survivor還存活着的對象一次性的拷貝到另一個Survivor區上,最後清理掉Eden區和剛纔使用過的Survivor區,Eden和Survivor的默認比例是8:1,可以使用-XX:SurvivorRatio來設置該比例。

       而老年代中對象存活率高,沒有額外的空間對它進行分配擔保,必須使用“標記-清理”或“標記-整理”算法。

(1)標記-清除算法(Mark-Sweep)

該算法分兩步執行:

       1) 標記Mark:從GC ROOTS開始,遍歷堆內存區域的所有根對象,對在引用鏈上的對象都進行標記。這樣下來,如果是存活的對象就會被做了標記,反之如果是垃圾對象,則沒做有標記。GC很容易根據有沒有被做標記就完成了垃圾對象回收。

      2) 清除Sweep:遍歷堆中的所有的對象(標記階段遍歷的是所有根節點),找到未被標記的對象,直接回收所佔的內存,釋放空間。

       優點:沒有產生額外的內存空間消耗,內存利用率高。

       缺點:體現在效率和空間,從效率的角度講,標記和清除兩個過程的效率都不高;從空間的角度講,標記清除後會產生大量不連續的內存碎片, 內存碎片太多可能會導致以後程序運行過程中在需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發一次垃圾收集動作。

標記-清除算法操作的對象是【垃圾對象】,對於活着的對象(被標記的對象),它則直接不理睬。

標記-清除算法執行過程如圖:

(2)複製算法(Copying)

        複製算法是爲了解決效率問題而出現的,它將可用的內存分爲兩塊,每次只用其中一塊,當這一塊內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已經使用過的內存空間一次性清理掉。這樣每次只需要對整個半區進行內存回收,內存分配時也不需要考慮內存碎片等複雜情況,只需要移動指針,按照順序分配即可。 

       優點:GC後的內存齊整,不產生內存碎片。

      缺點:內存縮小爲了原來的一半,GC要使用兩倍的內存,或者說導致堆只能使用被分配到的內存的一半,這個算法對空間要求太高!如果存活的對象較多,則意味着要複製很多對象並且要維護大量對象的內存地址,所以存活的對象數量不能太多,否則效率也會很低。

        現在的商用虛擬機都採用這種算法來回收新生代,不過研究表明1:1的比例非常不科學,因此新生代的內存被劃分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。每次回收時,將Eden和Survivor中還存活着的對象一次性複製到另外一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。HotSpot虛擬機默認Eden區和Survivor區的比例爲8:1,意思是每次新生代中可用內存空間爲整個新生代容量的90%。當然,我們沒有辦法保證每次回收都只有不多於10%的對象存活,當Survivor空間不夠用時,需要依賴老年代進行分配擔保(Handle Promotion)。

複製算法複製移動的對象是【活着的對象】,對於垃圾對象(不被標記的對象)則直接回收。

複製算法的執行過程如圖:

 (3)標記-整理算法(Mark-Compact)

這個算法則是對上面兩個算法的綜合結果。也分爲兩個階段:

1)標記:這個階段和標記-清除Mark-Sweep算法一樣,遍歷GC ROOTS並標記存活的對象。

2)整理:移動所有活着的對象到內存區域的一側(具體在哪一側則由GC實現),嚴格按照內存地址次序依次排列活着的對象,然後將最後一個活着的對象地址以後的空間全部回收。

       優點:內存空間利用率高,消除了複製算法內存減半的情況;GC後不會產生內存碎片。

       缺點:需要遍歷標記活着的對象,效率較低;複製移動對象後,還要維護這些活着對象的引用地址列表。

       複製算法在對象存活率較高的場景下要進行大量的複製操作,效率很低。萬一對象100%存活,那麼需要有額外的空間進行分配擔保。老年代都是不易被回收的對象,對象存活率高,因此一般不能直接選用複製算法。根據老年代的特點,有人提出了另外一種標記-整理算法,過程與標記-清除算法一樣,不過不是直接對可回收對象進行清理,而是讓所有存活對象都向一端移動,然後直接清理掉邊界以外的內存。

標記-整理算法的工作過程如圖:

(4)分代回收算法(Generational Collecting)

分代回收算法就是現在JVM使用的GC回收算法。

現代商用虛擬機基本都採用分代收集算法來進行垃圾回收,根據對象的生命週期的不同將內存劃分爲幾塊,然後根據各塊的特點採用最適當的收集算法。大批對象死去、少量對象存活的,使用複製算法,複製成本低;對象存活率高、沒有額外空間進行分配擔保的,採用標記-清理算法或者標記-整理算法。

1)簡要說明

   1】先來看看簡單化後的堆的內存結構:

Java堆 = 年老代 + 年輕代
(空間大小比例一般是3:1)

年輕代 = Eden區 + From Space區 + To Space區
(空間大小比例一般是8:1:1)

  2】按照對象存活時間長短,我們可以把對象簡單分爲三類:

  • 短命對象:存活時間較短的對象,如中間變量對象、臨時對象、循環體創建的對象等。這也是產生最多數量的對象,GC回收的關注重點。

  • 長命對象:存活時間較長的對象,如單例模式產生的單例對象、數據庫連接對象、緩存對象等。

  • 長生對象:一旦創建則一直存活,幾乎不死的對象。

  3】對象分配區域
短命對象存在於年輕代,長命對象存在於年老代,而長生對象則存在於方法區中。
由於GC的主要內存區域是堆,所以GC的對象主要就是短命對象和長命對象這類壽命“有限”的對象。

2)分代回收的GC類型

針對HotSpot VM的的GC其實準確分類只有兩大種:

1】Partial GC:部分回收模式

  • Young GC:只收集young gen的GC。和Minor GC一樣。
  • Old GC:只收集old gen的GC。只有CMS的concurrent - collection是這個模式
  • Mixed GC:收集整個young gen以及部分old gen的GC。只有G1有這個模式

2】Full GC:收集整個堆,包括young gen、old gen,還有永久代perm gen(如果存在的話)等所有部分的模式。同Major GC。

3】觸發時機
HotSpot VM的串行GC的觸發條件是:
young GC:當young gen中的eden區分配滿的時候觸發。

full GC:當準備要觸發一次young GC時,如果發現統計數據說之前young GC的平均晉升大小比目前old gen剩餘的空間大,則不會觸發young GC而是轉爲觸發full GC;或者,如果有perm gen的話,要在perm gen分配空間但已經沒有足夠空間時,也要觸發一次full GC;或者System.gc()、heap dump帶GC,默認也是觸發full GC。

併發GC的觸發條件就不太一樣。以CMS GC爲例,它主要是定時去檢查old gen的使用量,當使用量超過了觸發比例就會啓動一次CMS GC,對old gen做併發收集。

3)年輕代GC過程

       當需要在堆中創建一個新的對象,而年輕代內存不足時觸發一次GC,在年輕代觸發的GC稱爲普通GC,Minor GC。注意到年輕代中的對象都是存活時間較短的對象,所以適合使用複製算法。這裏肯定不會使用兩倍的內存來實現複製算法了,牛人們是這樣解決的,把年輕代內存組成是80%的Eden、10%的From Space和10%的To Space,然後在這些內存區域直接進行復制。

        剛開始創建的對象是在Eden中,此時Eden中有對象,而兩個survivor區沒有對象,都是空閒區間。第一次Minor GC後,存活的對象被放到其中一個survivor,Eden中的內存空間直接被回收。在下一次GC到來時,Eden和一個survivor中又創建滿了對象,這個時候GC清除的就是Eden和這個放滿對象的survivor組成的大區域(佔90%),Minor GC使用複製算法把活的對象複製到另一個空閒的survivor區間,然後直接回收之前90%的內存。周而復始。始終會有一個10%空閒的survivor區間,作爲下一次Minor GC存放對象的準備空間。

要完成上面的算法,每次Minor GC過程都要滿足:
存活的對象大小都不能超過survivor那10%的內存空間,不然就沒有空間複製剩下的對象了。但是,萬一超過了呢?前面我們提到過年老代,對,就是把這些大對象放到年老代。

 4)年老代GC

什麼樣的對象可以進入年老代呢?如下:

  • 在年輕代中,如果一個對象的年齡(GC一次後還存活的對象年歲加1)達到一個閾值(可以配置),就會被移動到年老代。
  • Survivor中相同年齡的對象大小總和超過survivor空間的一半,則不小於這個年齡的對象都會直接進入年老代。
  • 創建的對象的大小超過設定閾值,這個對象會被直接存進年老代。
  • 年輕代中大於survivor空間的對象,Minor GC時會被移進年老代。

        年老代中的對象特點就是存活時間較長,而且沒有備用的空閒空間,所以顯然不適合使用複製算法了,這個時候使用標記-清除算法或者標記-整理算法來實現GC。負責年老代中GC操作的是全局GC,Major GC,Full GC。

什麼時候觸發Major GC呢?
       在Minor GC時,先檢測JVM的統計數據,查看歷史上進入老年代的對象平均大小是否大於目前年老代中的剩餘空間,如果大於則觸發Full GC。

六:jvm的內存回收過程

        對象在Eden Space創建,當Eden Space滿了的時候,gc就把所有在Eden Space中的對象掃描一次,把所有有效的對象複製到第一個Survivor Space,同時把無效的對象所佔用的空間釋放。當Eden Space再次變滿了的時候,就啓動移動程序把Eden Space中有效的對象複製到第二個Survivor Space,同時,也將第一個Survivor Space中的有效對象複製到第二個Survivor Space。如果填充到第二個Survivor Space中的有效對象被第一個Survivor Space或Eden Space中的對象引用,那麼這些對象就是長期存在的,此時這些對象將被複制到Old Generation。

       若垃圾收集器依據這種小幅度的調整收集不能騰出足夠的空間,就會運行Full GC,此時jvm gc停止所有在堆中運行的線程並執行清除動作。

七:垃圾收集器

      垃圾收集器就是上面講的理論知識的具體實現了。不同虛擬機所提供的垃圾收集器可能會有很大差別,我們使用的是HotSpot,HotSpot這個虛擬機所包含的所有收集器如圖:

       上圖展示了7種作用於不同分代的收集器,如果兩個收集器之間存在連線,那說明它們可以搭配使用。虛擬機所處的區域說明它是屬於新生代收集器還是老年代收集器。多說一句,我們必須要明白一個道理:沒有最好的垃圾收集器,更加沒有萬能的收集器,只能選擇對具體應用最合適的收集器。這也是HotSpot爲什麼要實現這麼多收集器的原因。

(1)Serial收集器

       最基本、發展歷史最久的收集器,這個收集器是一個採用複製算法的單線程的收集器,單線程一方面意味着它只會使用一個CPU或一條線程去完成垃圾收集工作,另一方面也意味着它進行垃圾收集時必須暫停其他線程的所有工作,直到它收集結束爲止。後者意味着,在用戶不可見的情況下要把用戶正常工作的線程全部停掉,這對很多應用是難以接受的。不過實際上到目前爲止,Serial收集器依然是虛擬機運行在Client模式下的默認新生代收集器,因爲它簡單而高效。用戶桌面應用場景中,分配給虛擬機管理的內存一般來說不會很大,收集幾十兆甚至一兩百兆的新生代停頓時間在幾十毫秒最多一百毫秒,只要不是頻繁發生,這點停頓是完全可以接受的。

(2)ParNew收集器

       ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集外,其餘行爲和Serial收集器完全一樣,包括使用的也是複製算法。ParNew收集器除了多線程以外和Serial收集器並沒有太多創新的地方,但是它卻是Server模式下的虛擬機首選的新生代收集器,其中有一個很重要的和性能無關的原因是,除了Serial收集器外,目前只有它能與CMS收集器配合工作(看圖)。CMS收集器是一款幾乎可以認爲有劃時代意義的垃圾收集器,因爲它第一次實現了讓垃圾收集線程與用戶線程基本上同時工作。ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果,甚至由於線程交互的開銷,該收集器在兩個CPU的環境中都不能百分之百保證可以超越Serial收集器。當然,隨着可用CPU數量的增加,它對於GC時系統資源的有效利用還是很有好處的。它默認開啓的收集線程數與CPU數量相同,在CPU數量非常多的情況下,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。

(3)Parallel收集器

       Parallel收集器也是一個新生代收集器,也是用複製算法的收集器,也是並行的多線程收集器,但是它的特點是它的關注點和其他收集器不同。介紹這個收集器主要還是介紹吞吐量的概念。CMS等收集器的關注點是儘可能縮短垃圾收集時用戶線程的停頓時間,而Parallel收集器的目標則是打到一個可控制的吞吐量。所謂吞吐量的意思就是CPU用於運行用戶代碼時間與CPU總消耗時間的比值,即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間),虛擬機總運行100分鐘,垃圾收集1分鐘,那吞吐量就是99%。另外,Parallel收集器是虛擬機運行在Server模式下的默認垃圾收集器。

       停頓時間短適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗;高吞吐量則可以高效率利用CPU時間,儘快完成運算任務,主要適合在後臺運算而不需要太多交互的任務。

       虛擬機提供了-XX:MaxGCPauseMillis和-XX:GCTimeRatio兩個參數來精確控制最大垃圾收集停頓時間和吞吐量大小。不過不要以爲前者越小越好,GC停頓時間的縮短是以犧牲吞吐量和新生代空間換取的。由於與吞吐量關係密切,Parallel收集器也被稱爲“吞吐量優先收集器”。Parallel收集器有一個-XX:+UseAdaptiveSizePolicy參數,這是一個開關參數,這個參數打開之後,就不需要手動指定新生代大小、Eden區和Survivor參數等細節參數了,虛擬機會根據當親系統的運行情況手機性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量。如果對於垃圾收集器運作原理不太瞭解,以至於在優化比較困難的時候,使用Parallel收集器配合自適應調節策略,把內存管理的調優任務交給虛擬機去完成將是一個不錯的選擇。

(4)Serial Old收集器

       Serial收集器的老年代版本,同樣是一個單線程收集器,使用“標記-整理算法”,這個收集器的主要意義也是在於給Client模式下的虛擬機使用。

(5)Parallel Old收集器

       Parallel收集器的老年代版本,使用多線程和“標記-整理”算法。這個收集器在JDK 1.6之後的出現,“吞吐量優先收集器”終於有了比較名副其實的應用組合,在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel收集器+Parallel Old收集器的組合。

(6)CMS收集器

       CMS收集器是一種以獲取最短回收停頓時間爲目標的老年代收集器。目前很大一部分Java應用集中在互聯網站或者B/S系統的服務端上,這類應用尤其注重服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗,CMS收集器就非常符合這類應用的需求。CMS收集器從名字就能看出是基於“標記-清除”算法實現的。

(7)G1收集器

       G1(Garbage-First)收集器是當今收集器技術發展的最前沿成果之一,JDK 7 Update 4後開始進入商用。在G1收集器之前的其他收集器進行收集的範圍都是整個新生代或者老年代,而G1收集器不再是這樣,使用G1收集器時,Java堆的內存佈局就與其他收集器有很大差別,它將整個Java堆分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region的集合。G1收集器跟蹤各個Region裏面的垃圾堆積的價值大小,在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region(這也是Garbage-First名稱的由來)。這種使用Region劃分內存空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間內可以獲取儘可能高的收集效率。

垃圾收集器總結

來看一下對垃圾收集器的總結,列了一張表

GC組合

Minor GC

Full GC

描述
-XX:+UseSerialGC Serial收集器串行回收 Serial Old收集器串行回收 該選項可以手動指定Serial收集器+Serial Old收集器組合執行內存回收
-XX:+UseParNewGC ParNew收集器並行回收 Serial Old收集器串行回收 該選項可以手動指定ParNew收集器+Serilal Old組合執行內存回收
-XX:+UseParallelGC Parallel收集器並行回收 Serial Old收集器串行回收 該選項可以手動指定Parallel收集器+Serial Old收集器組合執行內存回收
-XX:+UseParallelOldGC Parallel收集器並行回收 Parallel Old收集器並行回收 該選項可以手動指定Parallel收集器+Parallel Old收集器組合執行內存回收
-XX:+UseConcMarkSweepGC ParNew收集器並行回收  缺省使用CMS收集器併發回收,備用採用Serial Old收集器串行回收
 
該選項可以手動指定ParNew收集器+CMS收集器+Serial Old收集器組合執行內存回收。優先使用ParNew收集器+CMS收集器的組合,當出現ConcurrentMode Fail或者Promotion Failed時,則採用ParNew收集器+Serial Old收集器的組合

-XX:+UseConcMarkSweepGC

-XX:-UseParNewGC

Serial收集器串行回收
-XX:+UseG1GC G1收集器併發、並行執行內存回收 暫無

 

GC日誌

每種收集器的日誌形式都是由它們自身的實現所決定的,換言之,每種收集器的日誌格式都可以不一樣。不過虛擬機爲了方便用戶閱讀,將各個收集器的日誌都維持了一定的共性,就以最前面的對象間相互引用的那個類ReferenceCountingGC的代碼爲例:

虛擬機參數爲“-XX:+PrintGCDetails -XX:+UseSerialGC”,使用Serial+Serial Old組合進行垃圾回收的日誌

[GC [DefNew: 310K->194K(2368K), 0.0269163 secs] 310K->194K(7680K), 0.0269513 secs] [Times: user=0.00 sys=0.00, real=0.03 secs] 
[GC [DefNew: 2242K->0K(2368K), 0.0018814 secs] 2242K->2241K(7680K), 0.0019172 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System) [Tenured: 2241K->193K(5312K), 0.0056517 secs] 4289K->193K(7680K), [Perm : 2950K->2950K(21248K)], 0.0057094 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 2432K, used 43K [0x00000000052a0000, 0x0000000005540000, 0x0000000006ea0000)
  eden space 2176K,   2% used [0x00000000052a0000, 0x00000000052aaeb8, 0x00000000054c0000)
  from space 256K,   0% used [0x00000000054c0000, 0x00000000054c0000, 0x0000000005500000)
  to   space 256K,   0% used [0x0000000005500000, 0x0000000005500000, 0x0000000005540000)
 tenured generation   total 5312K, used 193K [0x0000000006ea0000, 0x00000000073d0000, 0x000000000a6a0000)
   the space 5312K,   3% used [0x0000000006ea0000, 0x0000000006ed0730, 0x0000000006ed0800, 0x00000000073d0000)
 compacting perm gen  total 21248K, used 2982K [0x000000000a6a0000, 0x000000000bb60000, 0x000000000faa0000)
   the space 21248K,  14% used [0x000000000a6a0000, 0x000000000a989980, 0x000000000a989a00, 0x000000000bb60000)
No shared spaces configured.

虛擬機參數爲“-XX:+PrintGCDetails -XX:+UseParNewGC”,使用ParNew+Serial Old組合進行垃圾回收的日誌

[GC [ParNew: 310K->205K(2368K), 0.0006664 secs] 310K->205K(7680K), 0.0007043 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC [ParNew: 2253K->31K(2368K), 0.0032525 secs] 2253K->2295K(7680K), 0.0032911 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System) [Tenured: 2264K->194K(5312K), 0.0054415 secs] 4343K->194K(7680K), [Perm : 2950K->2950K(21248K)], 0.0055105 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 par new generation   total 2432K, used 43K [0x0000000005550000, 0x00000000057f0000, 0x0000000007150000)
  eden space 2176K,   2% used [0x0000000005550000, 0x000000000555aeb8, 0x0000000005770000)
  from space 256K,   0% used [0x0000000005770000, 0x0000000005770000, 0x00000000057b0000)
  to   space 256K,   0% used [0x00000000057b0000, 0x00000000057b0000, 0x00000000057f0000)
 tenured generation   total 5312K, used 194K [0x0000000007150000, 0x0000000007680000, 0x000000000a950000)
   the space 5312K,   3% used [0x0000000007150000, 0x0000000007180940, 0x0000000007180a00, 0x0000000007680000)
 compacting perm gen  total 21248K, used 2982K [0x000000000a950000, 0x000000000be10000, 0x000000000fd50000)
   the space 21248K,  14% used [0x000000000a950000, 0x000000000ac39980, 0x000000000ac39a00, 0x000000000be10000)
No shared spaces configured.

虛擬機參數爲“-XX:+PrintGCDetails -XX:+UseParallelGC”,使用Parallel+Serial Old組合進行垃圾回收的日誌

[GC [PSYoungGen: 4417K->288K(18688K)] 4417K->288K(61440K), 0.0007910 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System) [PSYoungGen: 288K->0K(18688K)] [PSOldGen: 0K->194K(42752K)] 288K->194K(61440K) [PSPermGen: 2941K->2941K(21248K)], 0.0032663 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 18688K, used 321K [0x0000000034190000, 0x0000000035660000, 0x0000000048f90000)
  eden space 16064K, 2% used [0x0000000034190000,0x00000000341e05c0,0x0000000035140000)
  from space 2624K, 0% used [0x0000000035140000,0x0000000035140000,0x00000000353d0000)
  to   space 2624K, 0% used [0x00000000353d0000,0x00000000353d0000,0x0000000035660000)
 PSOldGen        total 42752K, used 194K [0x000000000a590000, 0x000000000cf50000, 0x0000000034190000)
  object space 42752K, 0% used [0x000000000a590000,0x000000000a5c0810,0x000000000cf50000)
 PSPermGen       total 21248K, used 2982K [0x0000000005190000, 0x0000000006650000, 0x000000000a590000)
  object space 21248K, 14% used [0x0000000005190000,0x0000000005479980,0x0000000006650000)

虛擬機參數爲“-XX:+PrintGCDetails -XX:+UseConcMarkSweepGC”,使用ParNew+CMS+Serial Old組合進行垃圾回收的日誌

[Full GC (System) [CMS: 0K->194K(62656K), 0.0080796 secs] 4436K->194K(81792K), [CMS Perm : 2941K->2940K(21248K)], 0.0081589 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
Heap
 par new generation   total 19136K, used 340K [0x0000000005540000, 0x0000000006a00000, 0x0000000006a00000)
  eden space 17024K,   2% used [0x0000000005540000, 0x0000000005595290, 0x00000000065e0000)
  from space 2112K,   0% used [0x00000000065e0000, 0x00000000065e0000, 0x00000000067f0000)
  to   space 2112K,   0% used [0x00000000067f0000, 0x00000000067f0000, 0x0000000006a00000)
 concurrent mark-sweep generation total 62656K, used 194K [0x0000000006a00000, 0x000000000a730000, 0x000000000a940000)
 concurrent-mark-sweep perm gen total 21248K, used 2981K [0x000000000a940000, 0x000000000be00000, 0x000000000fd40000)

這四段GC日誌中提煉出一些共性:

1、日誌的開頭“GC”、“Full GC”表示這次垃圾收集的停頓類型,而不是用來區分新生代GC還是老年代GC的。如果有Full,則說明本次GC停止了其他所有工作線程。看到Full GC的寫法是“Full GC(System)”,這說明是調用System.gc()方法所觸發的GC。

2、“GC”中接下來的“DefNew”、“ParNew”、“PSYoungGen”、“CMS”表示的是老年代垃圾收集器的名稱,“PSYoungGen”中的“PS”指的是“Parallel Scavenge”,它是Parallel收集器的全稱。

3、以第一個爲例,方括號內部的“320K->194K(2368K)”、“2242K->0K(2368K)”,指的是該區域已使用的容量->GC後該內存區域已使用的容量(該內存區總容量)。方括號外面的“310K->194K(7680K)”、“2242K->2241K(7680K)”則指的是GC前Java堆已使用的容量->GC後Java堆已使用的容量(Java堆總容量)。

4、還以第一個爲例,再往後“0.0269163 secs”表示該內存區域GC所佔用的時間,單位是秒。最後的“[Times: user=0.00 sys=0.00 real=0.03 secs]”則更具體了,user表示用戶態消耗的CPU時間、內核態消耗的CPU時間、操作從開始到結束經過的鐘牆時間。後面兩個的區別是,鍾牆時間包括各種非運算的等待消耗,比如等待磁盤I/O、等待線程阻塞,而CPU時間不包括這些耗時,但當系統有多CPU或者多核的話,多線程操作會疊加這些CPU時間所以如果user或sys超過real是完全正常的。

5、“Heap”後面就列舉出堆內存目前各個年代的區域的內存情況

八:GC執行機制

(1)串行GC

      在搜索掃描和複製過程都是採用單線程實現,適用於單CPU、新生代空間較小或者要求GC暫停時間要求不高的地方。是client級別的默認方式。

(2)並行GC

       在搜索掃描和複製過程都是採用多線程實現,適用於多CPU、或者要求GC暫停時間要求高的地方。是server級別的默認方式。

(3)同步GC

        同時允許多個GC任務,減少GC暫停時間。主要應用在實時性要求重於總體吞吐量要求的中大型應用,即使如此,降低中斷時間的技術還是會導致應用程序性能的降低。

九:內存調優

       JVM內存調優,主要是減少GC的頻率和減少Full GC的次數,Full GC的時候會極大地影響系統的性能。所以在此基礎上,更加要關注會導致Full GC的情況。

(1)容易導致Full GC的情況

  • 年老代空間不足
    1)分配足夠大空間給old gen。
    2)避免直接創建過大對象或者數組,否則會繞過年輕代直接進入年老代。
    3)應該使對象儘量在年輕代就被回收,或待得時間儘量久,避免過早的把對象移進年老代。

  • 方法區的永久代空間不足
    1)分配足夠大空間給。
    2)避免創建過多的靜態對象。

  • 被顯示調用System.gc()
    通常情況下不要顯示地觸發GC,讓JVM根據自己的機制實現。

(2)JVM堆內存分配問題討論

       1)年輕代過小(年老代過大)

  • 導致頻繁發生GC,增大系統消耗
  • 容易讓普通大文件直接進入年老代,從而更容易誘發Full GC。

       2)年輕代過大(年老大過小)

  • 導致年老代過小,從而更容易誘發Full GC。
  • GC耗時增加,降低GC的效率。

       3) Eden過大(survivor過小)

Minor GC時容易讓普通大文件直接繞過survivor進入年老代,從而更容易誘發Full GC。

      4)Eden過小(survivor過大)

導致GC頻率升高,影響系統性能。

(3)調優策略

  • 保證系統吞吐量優先
  • 減少GC暫停時間優先

十:JVM常見配置選項

堆設置

-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數。並行收集線程數。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章