Java系列筆記(3) - Java 記憶體區域和GC機制 機製

2020-08-12 20:44:06

Java垃圾回收概況

  Java GC(Garbage Collection,垃圾收集,垃圾回收)機制 機製,是Java與C++/C的主要區別之一,作爲Java開發者,一般不需要專門編寫記憶體回收和垃圾清理程式碼,對記憶體泄露和溢位的問題,也不需要像C程式設計師那樣戰戰兢兢。這是因爲在Java虛擬機器中,存在自動記憶體管理和垃圾清掃機制 機製。概括地說,該機制 機製對JVM(Java Virtual Machine)中的記憶體進行標記,並確定哪些記憶體需要回收,根據一定的回收策略,自動的回收記憶體,永不停息(Nerver Stop)的保證JVM中的記憶體空間,防止出現記憶體泄露和溢位問題。

  關於JVM,需要說明一下的是,目前使用最多的Sun公司的JDK中,自從1999年的JDK1.2開始直至現在仍在廣泛使用的JDK6,其中預設的虛擬機器都是HotSpot。2009年,Oracle收購Sun,加上之前收購的EBA公司,Oracle擁有3大虛擬機器中的兩個:JRockit和HotSpot,Oracle也表明瞭想要整合兩大虛擬機器的意圖,但是目前在新發布的JDK7中,預設的虛擬機器仍然是HotSpot,因此本文中預設介紹的虛擬機器都是HotSpot,相關機制 機製也主要是指HotSpot的GC機制 機製。

  Java GC機制 機製主要完成3件事:確定哪些記憶體需要回收,確定什麼時候需要執行GC,如何執行GC。經過這麼長時間的發展(事實上,在Java語言出現之前,就有GC機制 機製的存在,如Lisp語言),Java GC機制 機製已經日臻完善,幾乎可以自動的爲我們做絕大多數的事情。然而,如果我們從事較大型的應用軟件開發,曾經出現過記憶體優化的需求,就必定要研究Java GC機制 機製。

  學習Java GC機制 機製,可以幫助我們在日常工作中排查各種記憶體溢位或泄露問題,解決效能瓶頸,達到更高的併發量,寫出更高效的程式。

  我們將從4個方面學習Java GC機制 機製,1,記憶體是如何分配的;2,如何保證記憶體不被錯誤回收(即:哪些記憶體需要回收);3,在什麼情況下執行GC以及執行GC的方式;4,如何監控和優化GC機制 機製。

Java記憶體區域

  瞭解Java GC機制 機製,必須先清楚在JVM中記憶體區域的劃分。在Java執行時的數據區裡,由JVM管理的記憶體區域分爲下圖幾個模組:

其中:

1,程式計數器(Program Counter Register):程式計數器是一個比較小的記憶體區域,用於指示當前執行緒所執行的位元組碼執行到了第幾行,可以理解爲是當前執行緒的行號指示器。位元組碼直譯器在工作時,會通過改變這個計數器的值來取下一條語句指令。

  每個程式計數器只用來記錄一個執行緒的行號,所以它是執行緒私有(一個執行緒就有一個程式計數器)的。

  如果程式執行的是一個Java方法,則計數器記錄的是正在執行的虛擬機器位元組碼指令地址;如果正在執行的是一個本地(native,由C語言編寫完成)方法,則計數器的值爲Undefined,由於程式計數器只是記錄當前指令地址,所以不存在記憶體溢位的情況,因此,程式計數器也是所有JVM記憶體區域中唯一一個沒有定義OutOfMemoryError的區域。

2,虛擬機器棧(JVM Stack):一個執行緒的每個方法在執行的同時,都會建立一個棧幀(Statck Frame),棧幀中儲存的有區域性變數表、操作站、動態鏈接、方法出口等,當方法被呼叫時,棧幀在JVM棧中入棧,當方法執行完成時,棧幀出棧。

  區域性變數表中儲存着方法的相關區域性變數,包括各種基本數據型別,物件的參照,返回地址等。在區域性變數表中,只有long和double型別會佔用2個區域性變數空間(Slot,對於32位元機器,一個Slot就是32個bit),其它都是1個Slot。需要注意的是,區域性變數表是在編譯時就已經確定好的,方法執行所需要分配的空間在棧幀中是完全確定的,在方法的生命週期內都不會改變。

  虛擬機器棧中定義了兩種異常,如果執行緒呼叫的棧深度大於虛擬機器允許的最大深度,則拋出StatckOverFlowError(棧溢位);不過多數Java虛擬機器都允許動態擴充套件虛擬機器棧的大小(有少部分是固定長度的),所以執行緒可以一直申請棧,直到記憶體不足,此時,會拋出OutOfMemoryError(記憶體溢位)。

  每個執行緒對應着一個虛擬機器棧,因此虛擬機器棧也是執行緒私有的。

3,本地方法棧(Native Method Statck):本地方法棧在作用,執行機制 機製,異常型別等方面都與虛擬機器棧相同,唯一的區別是:虛擬機器棧是執行Java方法的,而本地方法棧是用來執行native方法的,在很多虛擬機器中(如Sun的JDK預設的HotSpot虛擬機器),會將本地方法棧與虛擬機器棧放在一起使用。

  本地方法棧也是執行緒私有的。

4,堆區(Heap):堆區是理解Java GC機制 機製最重要的區域,沒有之一。在JVM所管理的記憶體中,堆區是最大的一塊,堆區也是Java GC機制 機製所管理的主要記憶體區域,堆區由所有執行緒共用,在虛擬機器啓動時建立。堆區的存在是爲了儲存物件範例,原則上講,所有的物件都在堆區上分配記憶體(不過現代技術裡,也不是這麼絕對的,也有棧上直接分配的)。

  一般的,根據Java虛擬機器規範規定,堆記憶體需要在邏輯上是連續的(在物理上不需要),在實現時,可以是固定大小的,也可以是可延伸的,目前主流的虛擬機器都是可延伸的。如果在執行垃圾回收之後,仍沒有足夠的記憶體分配,也不能再擴充套件,將會拋出OutOfMemoryError:Java heap space異常。

  關於堆區的內容還有很多,將在下節「Java記憶體分配機制 機製」中詳細介紹。

5,方法區(Method Area):在Java虛擬機器規範中,將方法區作爲堆的一個邏輯部分來對待,但事實上,方法區並不是堆(Non-Heap);另外,不少人的部落格中,將Java GC的分代收集機制 機製分爲3個代:青年代,老年代,永久代,這些作者將方法區定義爲「永久代」,這是因爲,對於之前的HotSpot Java虛擬機器的實現方式中,將分代收集的思想擴充套件到了方法區,並將方法區設計成了永久代。不過,除HotSpot之外的多數虛擬機器,並不將方法區當做永久代,HotSpot本身,也計劃取消永久代。本文中,由於筆者主要使用Oracle JDK6.0,因此仍將使用永久代一詞。

  方法區是各個執行緒共用的區域,用於儲存已經被虛擬機器載入的類資訊(即載入類時需要載入的資訊,包括版本、field、方法、介面等資訊)、final常數、靜態變數、編譯器即時編譯的程式碼等。

  方法區在物理上也不需要是連續的,可以選擇固定大小或可延伸大小,並且方法區比堆還多了一個限制:可以選擇是否執行垃圾收集。一般的,方法區上執行的垃圾收集是很少的,這也是方法區被稱爲永久代的原因之一(HotSpot),但這也不代表着在方法區上完全沒有垃圾收集,其上的垃圾收集主要是針對常數池的記憶體回收和對已載入類的解除安裝。

  在方法區上進行垃圾收集,條件苛刻而且相當困難,效果也不令人滿意,所以一般不做太多考慮,可以留作以後進一步深入研究時使用。

  在方法區上定義了OutOfMemoryError:PermGen space異常,在記憶體不足時拋出。

  執行時常數池(Runtime Constant Pool)是方法區的一部分,用於儲存編譯期就生成的字面常數、符號參照、翻譯出來的直接參照(符號參照就是編碼是用字串表示某個變數、介面的位置,直接參照就是根據符號參照翻譯出來的地址,將在類鏈接階段完成翻譯);執行時常數池除了儲存編譯期常數外,也可以儲存在執行時間產生的常數(比如String類的intern()方法,作用是String維護了一個常數池,如果呼叫的字元「abc」已經在常數池中,則返回池中的字串地址,否則,新建一個常數加入池中,並返回地址)。

6,直接記憶體(Direct Memory):直接記憶體並不是JVM管理的記憶體,可以這樣理解,直接記憶體,就是JVM以外的機器記憶體,比如,你有4G的記憶體,JVM佔用了1G,則其餘的3G就是直接記憶體,JDK中有一種基於通道(Channel)和緩衝區(Buffer)的記憶體分配方式,將由C語言實現的native函數庫分配在直接記憶體中,用儲存在JVM堆中的DirectByteBuffer來參照。由於直接記憶體收到本機器記憶體的限制,所以也可能出現OutOfMemoryError的異常。

Java物件的存取方式

一般來說,一個Java的參照存取涉及到3個記憶體區域:JVM棧,堆,方法區。

  以最簡單的本地變數參照:Object obj = new Object()爲例:

  • Object obj表示一個本地參照,儲存在JVM棧的本地變數表中,表示一個reference型別數據;
  • new Object()作爲範例物件數據儲存在堆中;
  • 堆中還記錄了Object類的型別資訊(介面、方法、field、物件型別等)的地址,這些地址所執行的數據儲存在方法區中;

在Java虛擬機器規範中,對於通過reference型別參照存取具體物件的方式並未做規定,目前主流的實現方式主要有兩種:

1,通過控制代碼存取(圖來自於《深入理解Java虛擬機器:JVM高階特效與最佳實現》):

通過控制代碼存取的實現方式中,JVM堆中會專門有一塊區域用來作爲控制代碼池,儲存相關控制代碼所執行的範例數據地址(包括在堆中地址和在方法區中的地址)。這種實現方法由於用控制代碼表示地址,因此十分穩定。

2,通過直接指針存取:(圖來自於《深入理解Java虛擬機器:JVM高階特效與最佳實現》)

通過直接指針存取的方式中,reference中儲存的就是物件在堆中的實際地址,在堆中儲存的物件資訊中包含了在方法區中的相應型別數據。這種方法最大的優勢是速度快,在HotSpot虛擬機器中用的就是這種方式。

Java記憶體分配機制 機製

這裏所說的記憶體分配,主要指的是在堆上的分配,一般的,物件的記憶體分配都是在堆上進行,但現代技術也支援將物件拆成標量型別(標量型別即原子型別,表示單個值,可以是基本型別或String等),然後在棧上分配,在棧上分配的很少見,我們這裏不考慮。

  Java記憶體分配和回收的機制 機製概括的說,就是:分代分配,分代回收。物件將根據存活的時間被分爲:年輕代(Young Generation)、年老代(Old Generation)、永久代(Permanent Generation,也就是方法區)。如下圖(來源於《成爲JavaGC專家part I》,http://www.importnew.com/1993.html):

    

  年輕代(Young Generation):物件被建立時,記憶體的分配首先發生在年輕代(大物件可以直接被建立在年老代),大部分的物件在建立後很快就不再使用,因此很快變得不可達,於是被年輕代的GC機制 機製清理掉(IBM的研究表明,98%的物件都是很快消亡的),這個GC機制 機製被稱爲Minor GC或叫Young GC。注意,Minor GC並不代表年輕代記憶體不足,它事實上只表示在Eden區上的GC。

  年輕代上的記憶體分配是這樣的,年輕代可以分爲3個區域:Eden區(伊甸園,亞當和夏娃偷吃禁果生娃娃的地方,用來表示記憶體首次分配的區域,再貼切不過)和兩個存活區(Survivor 0 、Survivor 1)。記憶體分配過程爲(來源於《成爲JavaGC專家part I》,http://www.importnew.com/1993.html):

    

  1. 絕大多數剛建立的物件會被分配在Eden區,其中的大多數物件很快就會消亡。Eden區是連續的記憶體空間,因此在其上分配記憶體極快;

  2. 最初一次,當Eden區滿的時候,執行Minor GC,將消亡的物件清理掉,並將剩餘的物件複製到一個存活區Survivor0(此時,Survivor1是空白的,兩個Survivor總有一個是空白的);

  3.  下次Eden區滿了,再執行一次Minor GC,將消亡的物件清理掉,將存活的物件複製到Survivor1中,然後清空Eden區;

  4.  將Survivor0中消亡的物件清理掉,將其中可以晉級的物件晉級到Old區,將存活的物件也複製到Survivor1區,然後清空Survivor0區;

  5. 當兩個存活區切換了幾次(HotSpot虛擬機器預設15次,用-XX:MaxTenuringThreshold控制,大於該值進入老年代,但這只是個最大值,並不代表一定是這個值)之後,仍然存活的物件(其實只有一小部分,比如,我們自己定義的物件),將被複制到老年代。

  從上面的過程可以看出,Eden區是連續的空間,且Survivor總有一個爲空。經過一次GC和複製,一個Survivor中儲存着當前還活着的物件,而Eden區和另一個Survivor區的內容都不再需要了,可以直接清空,到下一次GC時,兩個Survivor的角色再互換。因此,這種方式分配記憶體和清理記憶體的效率都極高,這種垃圾回收的方式就是著名的「停止-複製(Stop-and-copy)」清理法(將Eden區和一個Survivor中仍然存活的物件拷貝到另一個Survivor中),這不代表着停止複製清理法很高效,其實,它也只在這種情況下高效,如果在老年代採用停止複製,則挺悲劇的。

  在Eden區,HotSpot虛擬機器使用了兩種技術來加快記憶體分配。分別是bump-the-pointer和TLAB(Thread-Local Allocation Buffers),這兩種技術的做法分別是:由於Eden區是連續的,因此bump-the-pointer技術的核心就是跟蹤最後建立的一個物件,在物件建立時,只需要檢查最後一個物件後面是否有足夠的記憶體即可,從而大大加快記憶體分配速度;而對於TLAB技術是對於多執行緒而言的,將Eden區分爲若幹段,每個執行緒使用獨立的一段,避免相互影響。TLAB結合bump-the-pointer技術,將保證每個執行緒都使用Eden區的一段,並快速的分配記憶體。

  年老代(Old Generation):物件如果在年輕代存活了足夠長的時間而沒有被清理掉(即在幾次Young GC後存活了下來),則會被複制到年老代,年老代的空間一般比年輕代大,能存放更多的物件,在年老代上發生的GC次數也比年輕代少。當年老代記憶體不足時,將執行Major GC,也叫 Full GC。  

   可以使用-XX:+UseAdaptiveSizePolicy開關來控制是否採用動態控制策略,如果動態控制,則動態調整Java堆中各個區域的大小以及進入老年代的年齡。

  如果物件比較大(比如長字串或大陣列),Young空間不足,則大物件會直接分配到老年代上(大物件可能觸發提前GC,應少用,更應避免使用短命的大物件)。用-XX:PretenureSizeThreshold來控制直接升入老年代的物件大小,大於這個值的物件會直接分配在老年代上。

  可能存在年老代物件參照新生代物件的情況,如果需要執行Young GC,則可能需要查詢整個老年代以確定是否可以清理回收,這顯然是低效的。解決的方法是,年老代中維護一個512 byte的塊——」card table「,所有老年代物件參照新生代物件的記錄都記錄在這裏。Young GC時,只要查這裏即可,不用再去查全部老年代,因此效能大大提高。

Java GC機制 機製

GC機制 機製的基本演算法是:分代收集,這個不用贅述。下面 下麪闡述每個分代的收集方法。

  

  年輕代:

  事實上,在上一節,已經介紹了新生代的主要垃圾回收方法,在新生代中,使用「停止-複製」演算法進行清理,將新生代記憶體分爲2部分,1部分 Eden區較大,1部分Survivor比較小,並被劃分爲兩個等量的部分。每次進行清理時,將Eden區和一個Survivor中仍然存活的物件拷貝到 另一個Survivor中,然後清理掉Eden和剛纔的Survivor。

  這裏也可以發現,停止複製演算法中,用來複制的兩部分並不總是相等的(傳統的停止複製演算法兩部分記憶體相等,但新生代中使用1個大的Eden區和2個小的Survivor區來避免這個問題)

  由於絕大部分的物件都是短命的,甚至存活不到Survivor中,所以,Eden區與Survivor的比例較大,HotSpot預設是 8:1,即分別佔新生代的80%,10%,10%。如果一次回收中,Survivor+Eden中存活下來的記憶體超過了10%,則需要將一部分物件分配到 老年代。用-XX:SurvivorRatio參數來設定Eden區域Survivor區的容量比值,預設是8,代表Eden:Survivor1:Survivor2=8:1:1.

  老年代:

  老年代儲存的物件比年輕代多得多,而且不乏大物件,對老年代進行記憶體清理時,如果使用停止-複製演算法,則相當低效。一般,老年代用的演算法是標記-整理演算法,即:標記出仍然存活的物件(存在參照的),將所有存活的物件向一端移動,以保證記憶體的連續。

     在發生Minor GC時,虛擬機器會檢查每次晉升進入老年代的大小是否大於老年代的剩餘空間大小,如果大於,則直接觸發一次Full GC,否則,就檢視是否設定了-XX:+HandlePromotionFailure(允許擔保失敗),如果允許,則只會進行MinorGC,此時可以容忍記憶體分配失敗;如果不允許,則仍然進行Full GC(這代表着如果設定-XX:+Handle PromotionFailure,則觸發MinorGC就會同時觸發Full GC,哪怕老年代還有很多記憶體,所以,最好不要這樣做)

  方法區(永久代):

  永久代的回收有兩種:常數池中的常數,無用的類資訊,常數的回收很簡單,沒有參照了就可以被回收。對於無用的類進行回收,必須保證3點:

  1. 類的所有範例都已經被回收
  2. 載入類的ClassLoader已經被回收
  3. 類物件的Class物件沒有被參照(即沒有通過反射參照該類的地方)

     永久代的回收並不是必須的,可以通過參數來設定是否對類進行回收。HotSpot提供-Xnoclassgc進行控制

     使用-verbose,-XX:+TraceClassLoading、-XX:+TraceClassUnLoading可以檢視類載入和解除安裝資訊

     -verbose、-XX:+TraceClassLoading可以在Product版HotSpot中使用;

     -XX:+TraceClassUnLoading需要fastdebug版HotSpot支援

垃圾收集器

在GC機制 機製中,起重要作用的是垃圾收集器,垃圾收集器是GC的具體實現,Java虛擬機器規範中對於垃圾收集器沒有任何規定,所以不同廠商實現的垃圾 收集器各不相同,HotSpot 1.6版使用的垃圾收集器如下圖(圖來源於《深入理解Java虛擬機器:JVM高階特效與最佳實現》,圖中兩個收集器之間有連線,說明它們可以配合使用):

  

  

在介紹垃圾收集器之前,需要明確一點,就是在新生代採用的停止複製演算法中,「停 止(Stop-the-world)」的意義是在回收記憶體時,需要暫停其他所 有執行緒的執行。這個是很低效的,現在的各種新生代收集器越來越優化這一點,但仍然只是將停止的時間變短,並未徹底取消停止。

  • Serial收集器:新生代收集器,使用停止複製演算法,使用一個執行緒進行GC,序列,其它工作執行緒暫停。使用-XX:+UseSerialGC可以使用Serial+Serial Old模式執行進行記憶體回收(這也是虛擬機器在Client模式下執行的預設值)
  • ParNew收集器:新生代收集器,使用停止複製演算法,Serial收集器的多執行緒版,用多個執行緒進行GC,並行,其它工作執行緒暫停,關注縮短垃圾收集時間。使用-XX:+UseParNewGC開關來控制使用ParNew+Serial Old收集器組合收集記憶體;使用-XX:ParallelGCThreads來設定執行記憶體回收的執行緒數。
  • Parallel Scavenge 收集器:新生代收集器,使用停止複製演算法,關注CPU吞吐量,即執行使用者程式碼的時間/總時間,比如:JVM執行100分鐘,其中執行使用者程式碼99分鐘,垃 圾收集1分鐘,則吞吐量是99%,這種收集器能最高效率的利用CPU,適合執行後臺運算(關注縮短垃圾收集時間的收集器,如CMS,等待時間很少,所以適 合使用者互動,提高使用者體驗)。使用-XX:+UseParallelGC開關控制使用Parallel Scavenge+Serial Old收集器組合回收垃圾(這也是在Server模式下的預設值);使用-XX:GCTimeRatio來設定使用者執行時間佔總時間的比例,預設99,即1%的時間用來進行垃圾回收。使用-XX:MaxGCPauseMillis設定GC的最大停頓時間(這個參數只對Parallel Scavenge有效),用開關參數-XX:+UseAdaptiveSizePolicy可以進行動態控制,如自動調整Eden/Survivor比例,老年代物件年齡,新生代大小等,這個參數在ParNew下沒有。
  • Serial Old收集器:老年代收集器,單執行緒收集器,序列,使用標記整理(整理的方法是Sweep(清理)和Compact(壓縮),清理是將廢棄的物件幹掉,只留倖存的物件,壓縮是將移動物件,將空間填滿保證記憶體分爲2塊,一塊全是物件,一塊空閒)演算法,使用單執行緒進行GC,其它工作執行緒暫停(注意,在老年代中進行標記整理演算法清理,也需要暫停其它執行緒),在JDK1.5之前,Serial Old收集器與ParallelScavenge搭配使用。
  • Parallel Old收集器:老年代收集器,多執行緒,並行,多執行緒機制 機製與Parallel Scavenge差不錯,使用標記整理(與Serial Old不同,這裏的整理是Summary(彙總)和Compact(壓縮),彙總的意思就是將倖存的物件複製到預先準備好的區域,而不是像Sweep(清理)那樣清理廢棄的物件)演算法,在Parallel Old執行時,仍然需要暫停其它執行緒。Parallel Old在多覈計算中很有用。Parallel Old出現後(JDK 1.6),與Parallel Scavenge配合有很好的效果,充分體現Parallel Scavenge收集器吞吐量優先的效果。使用-XX:+UseParallelOldGC開關控制使用Parallel Scavenge +Parallel Old組合收集器進行收集。
  • CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力於獲取最短回收停頓時間(即縮短垃圾回收的時間),使用標記清除演算法,多執行緒,優點是併發收集(使用者執行緒可以和GC執行緒同時工作),停頓小。使用-XX:+UseConcMarkSweepGC進行ParNew+CMS+Serial Old進行記憶體回收,優先使用ParNew+CMS(原因見後面),當使用者執行緒記憶體不足時,採用備用方案Serial Old收集。

CMS收集的執行過程是:初始標記(CMS-initial-mark) -> 併發標記(CMS-concurrent-mark) -->預清理(CMS-concurrent-preclean)-->可控預清理(CMS-concurrent-abortable-preclean)-> 重新標記(CMS-remark) -> 併發清除(CMS-concurrent-sweep) ->併發重設狀態等待下次CMS的觸發(CMS-concurrent-reset)

具體的說,先2次標記,1次預清理,1次重新標記,再1次清除。 

1,首先jvm根據-XX:CMSInitiatingOccupancyFraction,-XX:+UseCMSInitiatingOccupancyOnly來決定什麼時間開始垃圾收集;

2,如果設定了-XX:+UseCMSInitiatingOccupancyOnly,那麼只有當old代佔用確實達到了-XX:CMSInitiatingOccupancyFraction參數所設定的比例時纔會觸發cms gc;

3,如果沒有設定-XX:+UseCMSInitiatingOccupancyOnly,那麼系統會根據統計數據自行決定什麼時候觸發cms gc;因此有時會遇到設定了80%比例才cms gc,但是50%時就已經觸發了,就是因爲這個參數沒有設定的原因;

4,當cms gc開始時,首先的階段是初始標記(CMS-initial-mark),是stop the world階段,因此此階段標記的物件只是從root集最直接可達的物件;

     CMS-initial-mark:961330K(1572864K),指標記時,old代的已用空間和總空間

5,下一個階段是併發標記(CMS-concurrent-mark),此階段是和應用執行緒併發執行的,所謂併發收集器指的就是這個,主要作用是標記可達的物件,此階段不需要使用者停頓。

       此階段會列印2條日誌:CMS-concurrent-mark-start,CMS-concurrent-mark

6,下一個階段是CMS-concurrent-preclean,此階段主要是進行一些預清理,因爲標記和應用執行緒是併發執行的,因此會有些物件的狀態在標記後會改變,此階段正是解決這個問題因爲之後的Rescan階段也會stop the world,爲了使暫停的時間儘可能的小,也需要preclean階段先做一部分工作以節省時間

     此階段會列印2條日誌:CMS-concurrent-preclean-start,CMS-concurrent-preclean

7,下一階段是CMS-concurrent-abortable-preclean階段,加入此階段的目的是使cms gc更加可控一些,作用也是執行一些預清理,以減少Rescan階段造成應用暫停的時間

     此階段涉及幾個參數:

     -XX:CMSMaxAbortablePrecleanTime:當abortable-preclean階段執行達到這個時間時纔會結束

     -XX:CMSScheduleRemarkEdenSizeThreshold(預設2m):控制abortable-preclean階段什麼時候開始執行,

      即當eden使用達到此值時,纔會開始abortable-preclean階段

     -XX:CMSScheduleRemarkEdenPenetratio(預設50%):控制abortable-preclean階段什麼時候結束執行

      此階段會列印一些日誌如下:

     CMS-concurrent-abortable-preclean-start,CMS-concurrent-abortable-preclean,

      CMS:abort preclean due to time XXX

8,再下一個階段是第二個stop the world階段了,即Rescan階段,此階段暫停應用執行緒,停頓時間比並發標記小得多,但比初始標記稍長。對物件進行重新掃描並標記;

       YG occupancy:964861K(2403008K),指執行時young代的情況

       CMS remark:961330K(1572864K),指執行時old代的情況

      此外,還列印出了弱參照處理、類解除安裝等過程的耗時

9,再下一個階段是CMS-concurrent-sweep,進行併發的垃圾清理

10,最後是CMS-concurrent-reset,爲下一次cms gc重置相關數據結構

 

有2種情況會觸發CMS 的悲觀full gc,在悲觀full gc時,整個應用會暫停

       A,concurrent-mode-failure:預清理階段可能出現,當cms gc正進行時,此時有新的物件要進行old代,但是old代空間不足造成的。其可能性有:1,O區空間不足以讓新生代晉級,2,O區空間用完之前,無法完成對無參照的物件的清理。這表明,當前有大量數據進入記憶體且無法釋放。

       B,promotion-failed:新生代young gc可能出現,當進行young gc時,有部分young代物件仍然可用,但是S1或S2放不下,因此需要放到old代,但此時old代空間無法容納此。

 

影響cms gc時長及觸發的參數是以下2個:

        -XX:CMSMaxAbortablePrecleanTime=5000

        -XX:CMSInitiatingOccupancyFraction=80

解決也是針對這兩個參數來的,根本的原因是每次請求消耗的記憶體量過大

解決方式:

      A,針對cms gc的觸發階段,調整-XX:CMSInitiatingOccupancyFraction=50,提早觸發cms gc,就可以緩解當old代達到80%,cms gc處理不完,從而造成concurrent mode failure引發full gc

     B,修改-XX:CMSMaxAbortablePrecleanTime=500,縮小CMS-concurrent-abortable-preclean階段的時間

     C,考慮到cms gc時不會進行compact,因此加入-XX:+UseCMSCompactAtFullCollection

       (cms gc後會進行記憶體的compact)和-XX:CMSFullGCsBeforeCompaction=4(在full gc4次後會進行compact)參數

 

在CMS清理過程中,只有初始標記和重新標記需要短暫停頓,併發標記和併發清除都不需要暫停使用者執行緒,因此效率很高,很適合高互動的場合。

CMS也有缺點,它需要消耗額外的CPU和記憶體資源,在CPU和記憶體資源緊張,CPU較少時,會加重系統負擔(CMS預設啓動執行緒數爲(CPU數量+3)/4)。

另外,在併發收集過程中,使用者執行緒仍然在執行,仍然產生記憶體垃圾,所以可能產生「浮動垃圾」,本次無法清理,只能下一次Full GC才清理,因此在GC期間,需要預留足夠的記憶體給使用者執行緒使用。所以使用CMS的收集器並不是老年代滿了才觸發Full GC,而是在使用了一大半(預設68%,即2/3,使用-XX:CMSInitiatingOccupancyFraction來設定)的時候就要進行Full GC,如果使用者執行緒消耗記憶體不是特別大,可以適當調高-XX:CMSInitiatingOccupancyFraction以降低GC次數,提高效能,如果預留的使用者執行緒記憶體不夠,則會觸發Concurrent Mode Failure,此時,將觸發備用方案:使用Serial Old 收集器進行收集,但這樣停頓時間就長了,因此-XX:CMSInitiatingOccupancyFraction不宜設的過大。

還有,CMS採用的是標記清除演算法,會導致記憶體碎片的產生,可以使用-XX:+UseCMSCompactAtFullCollection來設定是否在Full GC之後進行碎片整理,用-XX:CMSFullGCsBeforeCompaction來設定在執行多少次不壓縮的Full GC之後,來一次帶壓縮的Full GC。

  • G1收集器:在JDK1.7中正式發佈,與現狀的新生代、老年代概念有很大不同,目前使用較少,不做介紹。

 

     注意併發(Concurrent)和並行(Parallel)的區別:

     併發是指使用者執行緒與GC執行緒同時執行(不一定是並行,可能交替,但總體上是在同時執行的),不需要停頓使用者執行緒(其實在CMS中使用者執行緒還是需要停頓的,只是非常短,GC執行緒在另一個CPU上執行);

     並行收集是指多個GC執行緒並行工作,但此時使用者執行緒是暫停的;

所以,Serial是序列的,Parallel收集器是並行的,而CMS收集器是併發的.

 

關於JVM參數設定和記憶體調優範例,見我的下一篇部落格(編寫中:Java系列筆記(4) - JVM監控與調優),本來想寫在同一篇部落格裏的,無奈內容太多,只好另起一篇。

 

說明:

  本文是Java系列筆記的第3篇,這篇文章寫了很久,主要是Java記憶體和GC機制 機製相對複雜,難以理解,加上本人這段時間專案和生活中耗費的時間很多,所以進度緩慢。文中大多數筆記內容來源於我在網路上查到的部落格和《深入理解Java虛擬機器:JVM高階特效與最佳實現》一書。

  本人能力有限,如果有錯漏,請留言指正。

參考資料:

《JAVA程式設計思想》,第5章;

《Java深度歷險》,Java垃圾回收機制 機製與參照型別;

《深入理解Java虛擬機器:JVM高階特效與最佳實現》,第2-3章;

成爲JavaGC專家Part II — 如何監控Java垃圾回收機制 機製, http://www.importnew.com/2057.html

JDK5.0垃圾收集優化之--Don't Pause,http://calvin.iteye.com/blog/91905

【原】java記憶體區域理解-初步瞭解,http://iamzhongyong.iteye.com/blog/1333100

 關於施用full gc頻繁的分析及解決:http://www.07net01.com/zhishi/383213.html