天天看點

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

1. 确認待回收對象

垃圾收集器回收對象時,第一件事就是确認哪些對象需要被回收,确認算法有引用計數法和可達性分析。

1.1 引用計數法

在這中算法下,每個對象執行個體都會被配置設定一個引用計數器,每當一個地方引用它,則計數器值加1;當引用失效時,計數器值就減一;任何時刻計數器為0的對象都表示沒有任何引用。

引用計數法的缺點是無法檢測出循環引用,會造成記憶體洩漏。

1.2 可達性分析

可達性分析是通過一系列稱為

GC Roots

的對象作為起點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鍊(Reference Chain),當一個對象到GC Roots沒有任何引用鍊相連,則證明此對象是不可用的:

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

圖中object5和object6雖然互相引用,但是它們到GC Roots是不可達的,是以可以被回收。

在Java語言中,可作為GC Roots的對象包括下面幾種:

1) 虛拟機棧(棧幀中的局部變量表)中引用的對象;

2) 方法區中類靜态屬性引用的對象;

3) 方法區中常量引用的對象;

4) 本地方法棧中JNI(Native方法)引用的對象。           

GC進行垃圾回收主要區域是Java堆,方法區、棧和本地方法區不被GC管理,是以選擇這些區域的對象作為GC roots。

被棧幀的局部變量表中引用的對象作為GC Roots且沒有逃逸時,該方法執行結束該對象可以被回收,以其為GC Roots的對象也都可以被回收。

1.2.1 finalize

即使在可達性分析算法中不可達的對象,也并非是一定要被回收的,要真正回收一個對象,至少要經曆兩次标記過程:如果對象在進行可達性分析後發現沒有與GC Roots相連接配接的引用鍊,那它将會被第一次标記并且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆寫finalize()方法,或者finapze()方法已經被虛拟機調用過,虛拟機将這兩種情況都視為“沒有必要執行”。

如果這個對象被判定為有必要執行finalize()方法,那麼這個對象将會放置在一個叫做F-Queue的隊列之中,并在稍後由一個由虛拟機自動建立的、低優先級的Finalizer線程去執行它。

finalize()方法是對象逃脫死亡命運的最後一次機會,稍後GC将對F-Queue中的對象進行第二次小規模的标記,如果對象要在finalize()中拯救自己,隻要重新與引用鍊上的任何一個對象建立關聯即可,譬如把自己指派給某個類變量或者對象的成員變量,那麼在第二次标記時它将被移除出“即将回收”的集合;如果對象這時候還沒有逃脫,那麼基本上它就真的被回收了。

1.2.2 Java中的引用你了解多少

無論是通過引用計數算法判斷對象的引用數量,還是通過可達性分析算法判斷對象的引用鍊是否可達,判定對象是否存活都與“引用”有關。在JDK1.2以前,Java中的引用的定義很傳統:如果reference類型的資料中存儲的數值代表的是另外一塊記憶體的起始位址,就稱這塊記憶體代表着一個引用。

在JDK 1.2之後,Java對引用的概念進行了擴充,将引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。

1.2.2.1 強引用

在程式代碼中普遍存在的,類似 Object obj = new Object()這類引用,隻要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。

無論引用計數算法還是可達性分析算法都是基于強引用而言的。

1.2.2.2 軟引用

用來描述一些還有用但并非必須的對象。對于軟引用關聯着的對象,在系統将要發生記憶體溢出異常之前,将會把這些對象列進回收範圍之中進行第二次回收。如果這次回收後還沒有足夠的記憶體,才會抛出記憶體溢出異常。

1.2.2.3 弱引用

也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用關聯的對象隻能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論目前記憶體是否足夠,都會回收掉隻被弱引用關聯的對象。在JDK 1.2之後,提供了WeakReference類來實作弱引用。

1.2.2.4 虛引用

也叫幽靈引用或幻影引用,是最弱的一種引用關系。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象執行個體。它的作用是能在這個對象被收集器回收時收到一個系統通知。。在JDK 1.2之後,提供了PhantomReference類來實作虛引用。

2 垃圾回收算法

2.1 标記 -清除算法(Mark-Sweep)

最基礎的收集算法,如它的名字一樣,算法分為“标記”和“清除”兩個階段:首先标記出所有需要回收的對象,在标記完成後統一回收掉所有被标記的對象。之是以說它是最基礎的收集算法,是因為後續的收集算法都是基于這種思路并對其缺點進行改進而得到的。

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

标記清除算法的缺點如下:

效率問題:标記和清除過程的效率都不高;

空間問題:标記清除之後會産生大量不連續的記憶體碎片,空間碎片太多可能會導緻,碎片過多會導緻大對象無法配置設定到足夠的連續記憶體,進而不得不提前觸發另一次GC。           

2.2 複制算法

為解決效率問題,“複制”收集算法出現了。它将可用記憶體按容量劃分為大小相等的兩塊,每次隻使用其中的一塊。當這一塊的記憶體用完了,就将還存活着的對象複制到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。

這樣使得每次都是對其中的一塊進行記憶體回收,記憶體配置設定時也就不用考慮記憶體碎片等複雜情況,隻要移動堆頂指針,按順序配置設定記憶體即可,實作簡單,運作高效。

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

複制算法的缺點如下:

效率問題:在對象存活率較高時,複制操作次數多,效率降低;

空間問題:內存縮小了一半;需要額外空間做配置設定擔保(老年代)           

現代的商業虛拟機都采用複制算法來回收新生代的對象,IBM研究表明新生代中的對象98%都是“朝生夕死”的,是以并不需要按照1:1的比例來劃分記憶體空間,而是将記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor區。

兩塊Survivor中,和Eden一起使用的叫作survivor from,另一個叫作survivor to。當回收時,将Eden和Survivor from中還存活着的對象一次性複制到Survivor to,最後清理掉Eden和Survivor from中的資料。

需要注意的是,兩個Survivor區中from和to是相對的,根據每次進行MinorGC後哪個區被清空沒有對象了,這個區就會成為to區,而通過複制算法複制的還存活下的對象所在的那個區,也就是有對象的區即為from。

Hotspot預設Eden和survivor比例為8:1,也就是每次新生代中可用記憶體空間為整個新生代容量的90%,隻有10%的記憶體會被“浪費”。

當然,98%的對象可回收隻是一般場景下的資料,我們沒有辦法保證每次回收都隻有不多于10%的對象存活,當Survivor空間不夠用時,需要依賴其他記憶體(這裡指老年代)進行配置設定擔保。當另外一塊survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象時,這些對象将直接通過配置設定擔保機制進入老年代。

2.3 标記整理算法

複制收集算法在對象存活率較高時就要執行較多的複制操作,效率将會變低。更關鍵的是,如果不想浪費survivor to的空間,就需要有額外的空間進行配置設定擔保,以應對被使用的記憶體中對象存活大于Eden和survivor from空間的極端情況,是以在老年代一般不能直接選用這種算法。

根據老年代的特點,有人提出了另外一種“标記-整理”(Mark-Compact)算法,标記過程仍然與“标記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的記憶體。

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

2.4 分代收集算法

根據對象生活周期的不同将記憶體劃分為幾塊,一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最适當的算法。在新生代中,每次垃圾收集時都發現有大批對象死去,隻有少量存活,那就選用複制算法,隻需要付出少量存活對象的複制成本就可以完成收集。在老年代中,因為對象存活率高、沒有額外空間對它進行配置設定擔保,就必須使用标記清理或标記整理算法來 實作。

3. oopMap和safe point

3.1 oopMap

在正式的GC之前,要進行可達性分析來标記出需要被回收的對象。分析期間必須在一個能確定一緻性的快照中進行,不可以出現分析過程中還在不斷變化的情況,該點不滿足的話分析結果的準确性就無法保證,是以所有執行線程要停止來配合可達性分析,這種情況稱為

Stop The World

,簡稱

STW

線程配合GC枚舉根節點的停頓是不可避免的,即使是号稱幾乎不會停頓的CMS收集器中,枚舉根節點時也是必須要停頓的。

根據可達性分析查找識别引用類型方式可分為保守式GC和準确式GC,保守式和準确式的差別是能夠識别資料是引用類型還是基本類型,或者說能否識别指針和非指針。

3.1.1 保守式GC

在進行GC的時候,會從GC Roots開始掃描,掃描到數字時就檢查該數字是不是指向GC堆的一個位址,檢查方式包括堆的上下邊界檢查(GC堆的上下界是已知的)、對齊檢查(通常配置設定空間的時候會有對齊要求,假如說是4位元組對齊,那麼不能被4整除的數字就肯定不是指針),通過這些基本檢查之後的數字,保守式GC無法識别這些值是基本資料的數字還是指針,如:

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

圖中,對于變量A,JVM在得到A的值後,通過對齊檢查可以判斷為非指針,因為引用是一個位址,JVM中位址是32位的,也就是8位的16進制,很明顯A是一個4位16進制,沒有對齊,不能作為引用。對于變量D,因為Java堆的上下邊界是已知的,如同圖中所辨別的堆起始位址和最後位址,而D的值超出了堆的邊界,是以也不是指針。

但是對于變量B和C,JVM就不能判斷哪個是數字哪個是指針了。這種情況下,當執行B=null之後,對象B的執行個體就沒有了任何引用,但是因為JVM不能正确的識别C是數字,JVM會錯誤的認為C可能是對象B的引用,基于保守政策,不能回收對象B。

另一種情況,變量B一直保持對執行個體B的引用,此時進行了一次GC,對象B經過新生代的複制算法進入Survivor to區,此時對象B的記憶體位址發生了改變,此時JVM現在很慌,因為C變量如果當做引用,它也會指向對象B的執行個體,此時C的值也應該随執行個體B記憶體的變量而改變C的值,但是萬一C變量不是一個引用,而就是一個int型資料呢。于是保守式GC真正的記憶體模型出來了:

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

在java堆中增加了一個句柄池,當變量B的執行個體更改存放記憶體的地方後,JVM隻要改變句柄值,而不用改變變量B和變量C的值。

保守式GC的缺點:

對于死掉的對象,很可能誤認為仍有地方引用他們,例如執行B=null之後應當回收執行個體B,但是因為有變量C存在,GC就不會去回收它,造成了記憶體浪費。

由于不能識别是否指針,是以它們的值都不能改寫,是以引入了句柄池,但是句柄池在定位對象時造成了二次通路,通路效率變低了。

3.1.2 準确式GC

準确式GC就是能準确識别指針的GC,即虛拟機可以知道記憶體中某個位置的資料具體是什麼類型,比如記憶體中有一個32位的整數234567,虛拟機有能力分辨出來它到底是一個reference類型指向234567的記憶體位址還是一個數值為234567的整數。

實作準确式GC的一種方式是從外部記錄下類型資訊存成映射表,HotSpot、JRockit和J9都是這樣做的。其中,HotSpot把這樣的資料結構叫做OopMap。

使用這樣的映射表一般有兩種方式:

1、每次都周遊原始的映射表,循環的一個個偏移量掃描過去,這種用法也叫“解釋式”;

2、為每個映射表生成一塊定制的掃描代碼(想像掃描映射表的循環被展開的樣子),以後每次要用映射表就直接執行生成的掃描代碼;這種用法也叫“編譯式”。           

在類加載完成的時候,HotSpot就把對象内什麼偏移量上是什麼類型的資料計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用,這樣,GC在掃描時就可以直接得知這些資訊了。

3.2 safe point

每個被JIT編譯過後的方法也會在一些特定的位置記錄下OopMap,記錄了執行到該方法的某條指令的時候,棧上和寄存器裡哪些位置是引用。這樣GC在掃描棧的時候查詢這些OopMap就知道哪裡是引用了。這些特定的位置主要在:

1、循環的末尾
2、方法臨傳回前 / 調用方法的call指令後
3、可能抛異常的位置           

這種位置被稱為“安全點”。

之是以要選擇一些特定的位置來記錄OopMap,是因為oopMap雖然能幫助Hotspot準确的完成GC Roots枚舉,但是可能導緻引用關系變化,或者說引起oopMap内容變化的指令非常多,如果為每一條這樣的指令都生成對應的oopMap将需要大量額外空間,GC成本就會随之變高。選用安全點來記錄能有效的縮小空間成本,而且仍然能達到區分引用的目的。

GC放生時,所有線程都需要跑到最近的安全點上再停頓下來,這樣就限定了安全點的標明不能太少以至于讓GC等待時間太長,也不能過于頻繁以緻于過分增大運作時的負荷。

使線程停頓有兩種方案:搶先式中斷和主動式中斷。搶先式中斷不需要線程代碼主動配合,當GC發生時,首先把所有線程中斷,如果發現線程中斷的地方不在安全點上,就恢複線程,讓他跑到安全點上。現在幾乎沒有虛拟機實作采用搶先式中斷來暫停線程來響應GC。

而主動式中斷的思想是當GC需要中斷線程的時候,不直接對線程操作,僅僅簡單的設定一個标志,各個線程執行時主動去輪詢這個标志,發現中斷标志為真時就自己中斷挂起。輪詢标志的地方和安全點是重合的,另外再加上建立對象需要配置設定的記憶體的地方。

4. 垃圾回收器

垃圾收集器是垃圾回收算法的具體實作,,不同商家、不同版本的JVM所提供的垃圾收集器可能會有很在差别,Hotspot的收集器主要有如下七種:

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

兩個收集器之間的連線表示它們可以搭配使用,收集器所屬的區域表示它們屬于新生代收集器還是老年代收集器。

4.1 Serial收集器

單線程收集器,進行垃圾回收時隻會啟動一個線程,收集過程中需要暫停其它所有的工作線程,直到它收集結束。可以和serial old搭配使用:

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

serial收集器回收速度較慢且回收能力有限,頻繁的STW更會導緻較差的使用體驗。但它簡單高效,是Client模式下預設的垃圾收集器,适用場景為資源受限的環境,比如單核條件下。

如果要使用Serial收集器,可以通過

-XX:+UseSerialGC

參數指定。

4.2 ParNew收集器

ParNew收集器是Serial收集器的多線程版本,除了使用多線程進行垃圾收集工作,其他的控制參數(例如-XX:SurvivorRatio等)、收集算法、STW、對象配置設定規則、回收政策等均與Serial收集器一緻。

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

ParNew也是獨占式的回收器,在收集過程中應用程式會全部暫停。但是由于并行回收器使用多線程進行垃圾回收,是以在并發能力比較強的CPU上,它産生的停頓時間要短于串行回收器。

但是ParNew在單核/雙核環境下,效率未必有Serial收集器工作效率高(多線程切換開銷等因素限制),當然随着核數的增加,其性能也會得到較大的提升。它預設開啟的收集線程數和CPU核心數相同,可以使用-XX:ParallelGCThreads參數限制收集線程數。

除serial外,ParNew是目前唯一能和CMS收集器配合工作的。CMS是HotSpot在JDK1.5推出的第一款真正意義上的并發收集器,第一次實作了讓垃圾收集線程與使用者線程同時工作,但CMS無法與JDK1.4已經存在的新生代收集器Parallel Scavenge配合工作,因為Parallel Scavenge(以及G1)都沒有使用傳統的GC收集器代碼架構而是另外獨立實作,而其餘幾種收集器則共用了部分的架構代碼。

4.3 Parallel Scavenge收集器

Parallel Scavenge也是使用複制算法的收集器,但Parallel Scavenge的關注點不同,和ParNew相比,ParNew目标在于加速資源回收的速度,減少STW時間;Parallel Scavenge目标在于資源回收的吞吐量:

吞吐量 = 運作使用者代碼時間 / (運作使用者代碼時間 + 垃圾收集時間)           

例如jvm運作100min,其中垃圾回收1min,則吞吐量為99%。

停頓時間越短就越适合需要與使用者互動的程式,良好的響應速度能提升使用者體驗,而高吞吐量則可用高效率地利用CPU時間,盡快完成程式的運算任務,主要适合在背景運算而不需要太多互動的任務。

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

Parallel Scavenge收集器提供了兩個參數用于精确控制吞吐量,分别是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數以及直接設定吞吐量大小的-XX:GCTimeRatio參數。

MaxGCPauseMillis參數允許的值是一個大于0的毫秒數,收集器将盡可能地保證記憶體回收花費的時間不超過設定值。不過大家不要認為如果把這個參數的值設定得稍小一點就能使得系統的垃圾收集速度變得更快,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的:系統把新生代調小一些,收集300MB新生代肯定比收集500MB快吧,這也直接導緻垃圾收集發生得更頻繁一些,原來10秒收集一次、每次停頓100毫秒,現在變成5秒收集一次、每次停頓70毫秒。停頓時間的确在下降,但吞吐量也降下來了。

GCTimeRatio參數的值應當是一個大于0且小于100的整數,也就是垃圾收集時間占總時間的比率,相當于是吞吐量的倒數。如果把此參數設定為19,那允許的最大GC時間就占總時間的5%(即1 /(1+19)),預設值為99,就是允許最大1%(即1 /(1+99))的垃圾收集時間。

由于與吞吐量關系密切,Parallel Scavenge收集器也經常稱為“吞吐量優先”收集器。除上述兩個參數之外,Parallel Scavenge收集器還有一個參數-XX:+UseAdaptiveSizePolicy值得關注。這是一個開關參數,當這個參數打開之後,就不需要手工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數了,虛拟機會根據目前系統的運作情況收集性能監控資訊,動态調整這些參數以提供最合适的停頓時間或者最大的吞吐量,這種調節方式稱為GC自适應的調節政策(GC Ergonomics)。

如果對收集器運作原來不太了解,手工優化存在困難的時候,使用Parallel Scavenge收集器配合自适應調節政策,把記憶體管理的調優任務交給虛拟機去完成将是一個不錯的選擇。隻需要把基本的記憶體資料設定好(如-Xmx設定最大堆),然後使用MaxGCPauseMillis參數(更關注最大停頓時間)或GCTimeRatio(更關注吞吐量)參數給虛拟機設立一個優化目标,那具體細節參數的調節工作就由虛拟機完成了。自适應調節政策也是Parallel Scavenge收集器與ParNew收集器的一個重要差別。

4.4 Serial Old收集器

Serial Old是serial收集器的老年代版本,它同樣是一個單線程收集器,使用“标記--整理”算法。這個收集器的意義在于給Client模式下的虛拟機使用。如果在Server模式下,那麼它主要有兩大用途:一種是在jdk1.5以及之前的版本中與Parallel Scavenge收集器搭配使用,另一種用途是作為CMS收集器的後預案,在并發收集發生Concurrent Mode Failure時使用。工作流程圖如下:

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

4.5 Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程與“标記--整理”算法。這個收集器在jdk1.6中才開始提供的,直到Parallel Old 收集器出現後,“吞吐量優先”收集器終于有了比較名副其實的應用組合,在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加 Parallel Old收集器。工作過程如下圖:

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

4.6 CMS收集器

CMS收集器是一種以擷取最短回收停頓時間為目标的收集器,是基于“”标記--清理”算法實作的,整個回收過程分為四個步驟:

1. 初始标記

2. 并發标記

3. 重新标記

4. 并發清理           

其中初始标記和重新标記仍需要STW,初始标記僅僅是标記一下GC roots能直接關聯的對象,速度很快,并發标記就是進行gc roots tracing的過程,重新标記階段就是為了修正并發标記期間因為使用者程式繼續運作而導緻标記産生變動的那一部分對象的标記記錄,這個階段的停頓時間一般會比初始标記階段的時間稍長,但遠遠比并發标記階段時間短。

由于在整個過程和中最耗時的并發标記和并發清除過程收集器線程都可以和使用者線程一起工作,是以總體來說,CMS收集器的記憶體回收過程是與使用者線程一起并發執行的,其主要優點是并發收集、低停頓。

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

CMS收集器的缺點主要有三個:

對CPU資源敏感:并發收集雖然不會暫停應用程式,但是會占用CPU資源進而降低應用程式的執行效率(CMS預設收集線程數量=(CPU數量 + 3) / 4)。可以通過參數-XX:ConcGCThreads設定并發的GC線程數,降低CPU敏感度。

産生浮動垃圾:在并發清除時,使用者線程會産生新的垃圾,這一部分垃圾出現在标記過程之後,CMS無法在當次收集中處理掉它們,隻好留待下一次GC時再清理掉,這一部分垃圾就稱為浮動垃圾。由于在垃圾收集階段使用者線程還需要運作,那也就還需要預留有足夠的記憶體空間給使用者線程使用,是以CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供并發收集時的程式運作使用。

要是CMS運作期間預留的記憶體無法滿足程式需要,就會出現一次“Concurrent Mode Failure”失敗,這時虛拟機将啟動後備預案:臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。在jdk1.6中,CMS收集器的啟動門檻值是92%,意味着當老年代空間被使用92%時就自動觸發CMS回收機制,這個門檻值可以通過參數-XX:CMSInitiatingOccupancyFraction合理設定。

産生空間碎片:使用"标記-清除"算法,會産生大量不連續的記憶體碎片,進而導緻在配置設定大記憶體對象時,無法找到足夠的連續記憶體,不得不提前觸發一次Full GC操作。

為了解決這個問題,CMS提供了參數-XX:+UseCMSCompactAtFullGCCollection,用于在CMS頂不住要進行FullGC時開啟記憶體碎片的合并整理過程,整理過程是無法并發的,停頓時間不得不變長;虛拟機還提供了另一個參數-XX:CMSFullGCBeforeCompaction,這個參數用于設定執行多少次不壓縮的fullGC後,跟着來一次帶壓縮的fullGC(預設值為0,表示每次進入fullGC都進行碎片整理)。

4.7 G1收集器

4.7.1 G1特點

G1(Garbage-First)收集器是面向伺服器端應用的垃圾收集器。與其他GC收集器相比,G1具備如下特點:

并行與并發:G1能充分利用多CPU、多核環境下的硬體優勢,使用多個CPU來縮短STW停頓的時間,部分其他收集器原本需要停頓java線程執行的GC動作,G1收集器仍然可以通過并發的方式讓java程式繼續執行。

分代收集:與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不需要其他收集器配合就能夠獨立管理整個GC堆,但它能夠采用不同的方式去處理新建立的對象和已經存活了一段時間、熬過多次GC的舊對象以擷取更好的收集效果。

空間整合:與CMS的“标記--清理”算法不同,G1從整體來看是基于“标記--整理”算法實作的收集器,從局部(兩個Region之間)上來看是基于“複制”算法實作的,但無論如何,這兩種算法都意味着G1運作期間不會産生記憶體空間碎片,收集後能提供規整的可用記憶體。這個特性有利于程式長時間運作,配置設定大對象時不會因為無法找到連續記憶體空間而提前出發下一次GC。

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

可預測的停頓:這是G1相對于CMS的另一大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明确指定在一個長度為M毫秒的時間片段内,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時java(RTSJ)的垃圾收集器的特性了。

使用G1收集器時,java堆的記憶體布局就與其他收集器有很大差别,它将真個java堆劃分為多個大小相等的獨立區域(Region),雖然還保留新生代與老年代的概念,但新生代與老年代不再試實體隔離的了,他們都是一部分Region(不需要連續)的集合。

G1收集器之是以能建立可預測的停頓時間模型,是因為它可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裡面的垃圾堆積的價值大小(回收所擷取的空間大小以及回收所需要的時間的經驗值),在背景維護一個優先清單,每次根據允許的收集時間,優先回收價值最大的Region,這也是Garbage-First名稱的由來。

4.7.2 region

G1将堆空間分成若幹個大小相等的記憶體區域,稱為region,每次配置設定對象空間将逐段地使用記憶體。啟動時可以通過參數

-XX:G1HeapRegionSize

指定分區大小(1MB~32MB,且必須是2的幂),預設将整堆劃分為2048個分區。

在每個分區内部又被分成了若幹個大小為512Byte的卡片(Card),配置設定的對象會占用實體上連續的若幹個卡片,當查找對分區内對象的引用時便可通過記錄卡片來查找該引用對象。每次對記憶體的回收,都是對指定分區的卡片進行處理。

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

region不是孤立的,一個對象配置設定在某個region中,并非隻能被本region中的其他對象引用,而是可能與整個java堆任意的對象發生引用關系。那這樣做可達性判定确定對象是否存活的時候,難道要掃描整個java堆才能保證準确性嗎?

在G1收集器region之間的對象引用,以及其他收集器中的新生代與老年代之間的對象引用,虛拟機都是使用Remembered Set(RSet)來避免全堆掃描的。G1中每個region都有一個與之對應的remembered Set。

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

虛拟機發現程式在對Reference類型的資料進行寫操作時,會産生一個Write Barrier暫時終端寫操作,檢查Reference引用對象是否處于不同的Region之中(在分代的例子中就是檢查是否老年代中的對象引用了新生代中的對象),如果是,便通過CardTable把相關引用資訊記錄到被引用對象所屬的regipn的Remembered Set之中。當進行記憶體回收時,在GC根結點的枚舉範圍中加入Rememberd Set即可保證不對全堆掃描也不會有遺漏。

4.7.3 分代

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

G1将記憶體在邏輯上劃分為年輕代和老年代,其中年輕代又劃分為Eden空間和Survivor空間。但年輕代空間并不是固定不變的,當現有年輕代分區占滿時,JVM會配置設定新的空閑分區加入到年輕代空間。

整個年輕代記憶體會在初始空間

-XX:G1NewSizePercent

(預設整堆5%)與最大空間

-XX:G1MaxNewSizePercent

(預設60%)之間動态變化,且由參數目标暫停時間

-XX:MaxGCPauseMillis

(預設200ms)、需要擴縮容的大小以及分區的RSet計算得到。當然,G1依然可以設定固定的年輕代大小(參數-XX:NewRatio、-Xmn),但同時暫停目标将失去意義。

4.7.4 本地配置設定緩沖

每個線程均可以"認領"某個分區用于線程本地的記憶體配置設定,是以,每個應用線程和GC線程都會獨立的使用分區,進而減少同步時間,提升GC效率,這個分區稱為本地配置設定緩沖區Local allocation buffer(Lab)。

其中,應用線程可以獨占一個本地緩沖區(TLAB)來建立的對象,而大部分都會落入Eden區域(巨型對象或配置設定失敗除外),是以TLAB的分區屬于Eden空間;而每次垃圾收集時,每個GC線程同樣可以獨占一個本地緩沖區(GCLAB)用來轉移對象,每次回收會将對象複制到Suvivor空間或老年代空間;對于從Eden/Survivor空間晉升(Promotion)到Survivor/老年代空間的對象,同樣有GC獨占的本地緩沖區進行操作,該部分稱為晉升本地緩沖區(PLAB)。

一個大小達到甚至超過分區大小一半的對象稱為巨型對象。當線程為巨型配置設定空間時,不能簡單在TLAB進行配置設定,因為巨型對象的移動成本很高,是以,巨型對象會直接在老年代配置設定,所占用的連續空間稱為巨型分區(Humongous Region)。G1内部做了一個優化,一旦發現沒有引用指向巨型對象,則可直接在年輕代收集周期中被回收。

TLAB可以通過參數

-XX:+/-UseTLAB

啟用。

4.7.5 收集集合 (CSet)

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

收集集合(CSet)代表每次GC暫停時需要回收的一系列目标分區,收集期間,CSet所有分區都會被釋放,内部存活的對象都會被轉移到配置設定的空閑分區中。年輕代收集CSet隻容納年輕代分區,而混合收集會在老年代候選回收分區中,篩選出回收收益最高的分區添加到CSet中。

候選老年代分區的CSet準入條件,可以通過活躍度門檻值

-XX:G1MixedGCLiveThresholdPercent

(預設85%)進行設定,進而攔截那些回收開銷巨大的對象;同時,每次混合收集可以包含候選老年代分區,可根據CSet對堆的總大小占比

-XX:G1OldCSetRegionThresholdPercent

(預設10%)設定數量上限。

由上述可知,G1的收集都是根據CSet進行操作的,年輕代收集與混合收集沒有明顯的不同,最大的差別在于兩種收集的觸發條件。

4.7.5.1 年輕代收集集合

當JVM配置設定對象到Eden區域失敗時,便會觸發一次STW式的年輕代收集。在年輕代收集中,Eden分區存活的對象将被拷貝到Survivor分區;原有Survivor分區存活的對象,将根據任期門檻值(tenuring threshold)分别晉升到PLAB中,新的survivor分區和老年代分區。而原有的年輕代分區将被整體回收掉。

同時,年輕代收集還負責維護對象的年齡(存活次數),輔助判斷老化(tenuring)對象晉升的時候是到Survivor分區還是到老年代分區。年輕代收集首先先将晉升對象尺寸總和、對象年齡資訊維護到年齡表中,再根據年齡表、Survivor尺寸、Survivor填充容量

-XX:TargetSurvivorRatio

(預設50%)、最大任期門檻值

-XX:MaxTenuringThreshold

(預設15),計算出一個恰當的任期門檻值,凡是超過任期門檻值的對象都會被晉升到老年代。

4.7.5.2 混合收集集合

年輕代收集不斷活動後,老年代的空間也會被逐漸填充。當老年代占用空間超過整堆比IHOP門檻值

-XX:InitiatingHeapOccupancyPercent

(預設45%)時,G1就會啟動一次混合垃圾收集周期。為了滿足暫停目标,G1可能不能一口氣将所有的候選分區收集掉,是以G1可能會産生連續多次的混合收集與應用線程交替執行,每次STW的混合收集與年輕代收集過程相類似。

為了确定包含到年輕代收集集合CSet的老年代分區,JVM通過參數混合周期的最大總次數

-XX:G1MixedGCCountTarget

(預設8)、堆廢物百分比

-XX:G1HeapWastePercent

(預設5%)。通過候選老年代分區總數與混合周期最大總次數,确定每次包含到CSet的最小分區數量;根據堆廢物百分比,當收集達到參數時,不再啟動新的混合收集。而每次添加到CSet的分區,則通過計算得到的GC效率進行安排。

G1也可以通過-Xms/-Xmx來指定堆空間大小。當發生年輕代收集或混合收集時,通過計算GC與應用的耗費時間比,自動調整堆空間大小。如果GC頻率太高,則通過增加堆尺寸,來減少GC頻率,相應地GC占用的時間也随之降低;目标參數

-XX:GCTimeRatio

即為GC與應用的耗費時間比,G1預設為9,而CMS預設為99,因為CMS的設計原則是耗費在GC上的時間盡可能的少。

另外,當空間不足,如對象空間配置設定或轉移失敗時,G1會首先嘗試增加堆空間,如果擴容失敗,則發起擔保的Full GC。Full GC後,堆尺寸計算結果也會調整堆空間。

4.7.6 回收機制

如果不計算維護Remembered Set的操作,G1收集器的運作大緻可劃分為以下幾個步驟:

1、初始标記(Initial Marking)
2、并發标記(Concurrent Marking)
3、最終标記(Final Marking)
4、篩選回收(Live Data Counting and Evacuation)           

G1的前幾個步驟和CMS有很多相似之處。初始标記僅僅隻是标記一下GC Roots能直接關聯到的對象,并且修改TAMS(Next Top at Mark Start)的值,讓下一階段使用者程式并發運作時,能在正确可用的region中建立新對象,這階段需要線程停頓,但耗時很短。

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

并發标記是從GC Roots開始對堆中對象進行可達性分析,找出存活對象,這階段耗時較長,但可與使用者線程并發執行。而最終标記階段則是為了修正在并發标記期間因使用者線程記性運作而導緻标記産生變化的那一部分标記記錄,虛拟機将這段時間對象變化記錄線上程Remembered Set Logs裡面,最終标記階段需要把Remembered Set Log的資料合并到Remembered Set 中,這幾段需要停頓線程,但是可并行執行。

最後在篩選回收階段首先對各個region的回收價值和成本進行排序,根據使用者所期望的GC停頓時間來制定回收計劃,這個階段理論上也可以做到與使用者線程并發執行,但是因為隻回收一部分region,時間是使用者可控制的,而且停頓使用者線程将大幅提高收集效率,如下圖可以比較清除的看到G1收集器運作的步驟中并發和需要停頓的階段。

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

由上的闡述,可以看到G1之前的收集器和G1的異同點,之前的收集器大多如下特點:

年輕代、老年代是獨立且連續的記憶體塊;

年輕代收集基本都采用Eden、survivor from和survivor to進行記憶體管理,使用複制算法作為收集算法;

老年代垃圾收集基本采用掃描整個老年代區域。           

G1:

G1可以根據使用者設定的暫停時間目标自動調整年輕代和總堆大小(和Parallel Scavenge類似),且暫停時間越短年輕代空間越小、總空間就越大;

G1的空間整合特點表示其采用了複制清除、标記整理算法;

G1隻有邏輯上的分代概念,不存在實體隔離的年輕代和老年代,年輕代中也不需要實體隔離的survivor區,G1收集都是STW,并且但年輕代和老年代采用了混合收集的方式,每次收集可能同時收集年輕代和老年代。           

jdk1.8下,當記憶體相對比較大時可以采用G1替代CMS,例如32G記憶體;jdk1.9下基本放棄了CMS,預設就是G1。

4.8 ZGC收集器

4.8.1 ZGC簡介

Z垃圾收集器也稱為ZGC,是一種可伸縮的低延遲垃圾收集器。其主要目标:

GC暫停時間不應超過10ms;

能夠處理幾百M到幾個T大小的堆;

與使用G1相比,應用程式吞吐量減少不超過15%;           

ZGC為GC特征優化和着色指針、負載障礙奠定了基礎,其最初支援的平台為Linux/x64。ZGC中沒有新生代和老年代的概念,隻有一塊一塊的記憶體區域page,以page機關進行對象的配置設定和回收。每次進行GC時,都會對page進行壓縮操作,是以完全避免了CMS算法中的碎片化問題。

ZGC支援NUMA,現代多CPU插槽伺服器都是Numa架構,比如兩個CPU插槽(24核),64G記憶體的伺服器,每個CPU槽上的12個核都通路從屬于它的32G記憶體,比通路另外32G遠端記憶體要快得多。在建立對象時,根據目前線程在哪個CPU執行,優先在靠近這個CPU的記憶體進行配置設定,這樣可以顯著的提高性能。

4.8.2 着色指針

之前的收集器在标記階段,例如CMS、G1都是在對象的對象頭進行标記,而ZGC是标記對象的指針。着色指針是将資訊存儲在指針中,ZGC僅支援64位平台,是以指針為64位,其使用其中低42位表示對象的位址,42-45位用來做指針狀态:

ZGC限制最大支援4Tb堆(42-bits),剩下22位可用,目前使用了4位finalizable、remapped、mark0和mark1:

finalizable:該對象隻能通過終結器來通路

remapped:重映射位,參考指向對象的目前位址

marked0和marked1位 - 這些用于标記可到達的對象           

ZGC在指針上做标記,線上程通路指針時加入Load Barrier,比如當對象正被GC移動時,這個屏障會先把指針更新為有效位址再傳回,是以隻有單個對象讀取時有機率被阻塞,而不是STW。

着色指針在取消着色時,需要額外的工作(因為需要屏蔽資訊位)。像SPARC這樣的平台有内置硬體支援指針屏蔽是以不是問題,而對于x86平台來說,ZGC團隊使用了簡潔的多重映射技巧。

4.8.3 load barriers

因為在标記和移動過程中,GC線程和應用線程是并發執行的,是以存在這種情況:對象A内部的引用所指的對象B在标記或者移動狀态,為了保證應用線程拿到的B對象是對的,那麼在讀取B的指針時會經過一個 “load barriers” 讀屏障,這個屏障可以保證在執行GC時,資料讀取的正确性。

也就是說,讀屏障是每當應用程式線程讀取引用時,立即檢查引用的狀态,并在将引用傳回給應用程式之前執行一些工作。

4.8.4 ZGC回收過程

4.8.4.1 标記

标記用于确定可達對象,ZGC标記分三個階段:

第一階段是尋找GC Roots并标記其直接子節點,例如局部變量或靜态字段等,這個階段是STW的,由于GC Roots數量通常較小,是以該階段耗時很短。如下标記了GC Roots的直接子節點1、2、4:

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

第二階段是并發階段,這個階段從GC Roots開始遞歸的标記每個可達的對象。此外,當負載屏障檢測到未标記的引用時,則将其添加到隊列以進行标記。ZGC使用marked0和marked1中繼資料位進行标記。

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

最後階段也是STW,用來處理一些邊緣情況,比如弱引用。

4.8.4.2 重定位

标記完成之後是重定位,重定位涉及移動活動對象以釋放部分堆記憶體。ZGC重定位也包括三個階段。

第一階段是并發查找需要重定位的page并将它們放入重定位集中,ZGC将整個堆分成許多page,此階段會選擇一組需要重定位活動對象的頁面。

第二階段,選擇重定位集後,ZGC開始STW并重定位該集合中root對象,将它們的引用更新為新位置,STW的暫停時間取決于GC Roots的數量,停頓時間很短。

第三階段,移動root後進行并發重定位,GC線程周遊重定位集并重新定位page中所有的對象,如果應用程式線程在GC重新定位對象期間加載其引用,則讀屏障負責将重定位後的引用傳回:

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

4.8.4.3 重映射

并發重定位會産生轉發表,在轉發表中記錄了舊位址和新位址之間的映射:

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

并發标記完成後,開始進行引用更新,将引用位址更新為轉發表中記錄的新位址,更新完成後則釋放轉發表空間:

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

GC線程最終将對重定位集中的所有對象重定位,然而可能仍有引用指向這些對象的舊位置,如果此時程式線程需要使用這些引用,将無法通路我們想要的對象,ZGC使用負載屏障來解決這個問題,負載屏障将負責傳回正确的引用。

剩餘的舊引用,GC可以周遊對象圖并重新映射這些引用到新位置,但是這一步代價很高昂。是以這一步與下一個标記階段合并在一起。在下一個GC周期的标記階段周遊對象對象圖的時候,如果發現未重映射的引用,則将其重新映射,然後标記為活動狀态。

4.8.5 ZGC性能

SPECjbb 2015[1]的正常性能測試,從吞吐量和延遲的角度來看,ZGC性能看起來不錯。下面是ZGC和G1的max-jOPS以及critical-jOPS得分:

ZGC
       max-jOPS: 100%
  critical-jOPS: 76.1%

G1
       max-jOPS: 91.2%
  critical-jOPS: 54.7%           

以下是ZGC和G1在同一基準下的GC停頓時間對别:

ZGC
                avg: 1.091ms (+/-0.215ms)
    95th percentile: 1.380ms
    99th percentile: 1.512ms
  99.9th percentile: 1.663ms
 99.99th percentile: 1.681ms
                max: 1.681ms

G1
                avg: 156.806ms (+/-71.126ms)
    95th percentile: 316.672ms
    99th percentile: 428.095ms
  99.9th percentile: 543.846ms
 99.99th percentile: 543.846ms
                max: 543.846ms           

一般來說,ZGC保持一位數的毫秒暫停時間。

4.8.6 使用ZGC

按照慣例,JVM中的實驗特性預設被建構系統禁用。ZGC是一個實驗性的特性,是以它不會出現在JDK建構中,需要在運作時顯式解鎖。是以,要啟用/使用ZGC,需要以下JVM選項:

-XX:+UnlockExperimentalVMOptions-XX:+UseZGC           

如果您是第一次嘗試ZGC,請首先使用以下GC選項:

-XX:+UnlockExperimentalVMOptions -XX:+UseZGC -Xmx<size> -Xlog:gc           

有關更詳細的日志記錄,請使用以下選項:

-XX:+UnlockExperimentalVMOptions -XX:+UseZGC -Xmx<size> -Xlog:gc*           

ZGC能和下列參數一起使用:

垃圾回收算法1. 确認待回收對象2 垃圾回收算法3. oopMap和safe point4. 垃圾回收器5. 記憶體配置設定與回收政策

更多參數設定:

https://wiki.openjdk.java.net/display/zgc/Main

4.9 了解GC日志

每一種收集器的日志形式都是由它們自身的實作決定的,但是虛拟機的設計者為了友善使用者閱讀,将每個收集器的日志都維持一定的共性。可以通過下列參數開啟GC日志列印:

-Xms3m -Xmx24m -Xmn3m -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:+PrintGCTimeStamps           

列印結果示例:

0.178: [GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->891K(3584K), 0.0008194 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.583: [GC (Allocation Failure) [PSYoungGen: 2536K->504K(2560K)] 2939K->1101K(3584K), 0.0018190 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.585: [Full GC (Ergonomics) [PSYoungGen: 504K->497K(2560K)] [ParOldGen: 597K->541K(3072K)] 1101K->1038K(5632K), [Metaspace: 3743K->3743K(1056768K)], 0.0068327 secs] [Times: user=0.06 sys=0.00, real=0.01 secs] 
0.601: [GC (Allocation Failure) --[PSYoungGen: 1603K->1603K(2560K)] 22624K->22656K(24064K), 0.0009573 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.602: [Full GC (Ergonomics) [PSYoungGen: 1603K->1493K(2560K)] [ParOldGen: 21053K->20885K(21504K)] 22656K->22379K(24064K), [Metaspace: 3758K->3758K(1056768K)], 0.0094440 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
0.612: [GC (Allocation Failure) --[PSYoungGen: 1493K->1493K(2560K)] 22379K->22403K(24064K), 0.0008611 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.613: [Full GC (Allocation Failure) [PSYoungGen: 1493K->1470K(2560K)] [ParOldGen: 20909K->20887K(21504K)] 22403K->22357K(24064K), [Metaspace: 3758K->3758K(1056768K)], 0.0090611 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]            

4.9.1 回收發生時間

最前邊的數字

0.178

0.583

表示GC發生的時間,這個數字代表了自虛拟機啟動以來經過的秒數。

4.9.2 [GC、[FULL

GC日志的開頭

[GC

[FULL GC

表示這次垃圾收集的停頓類型,如果有FULL,說明這次垃圾收集發生了STW。[GC和[FULL和GC發生的地點,例如在新生代還是老年代無關。例如下面新生代垃圾收集發生了fullGC:

0.585: [Full GC (Ergonomics) [PSYoungGen: 504K->497K(2560K)] [ParOldGen: 597K->541K(3072K)] 1101K->1038K(5632K), [Metaspace: 3743K->3743K(1056768K)], 0.0068327 secs] [Times: user=0.06 sys=0.00, real=0.01 secs]            

這一般是因為發生了配置設定擔保失敗,導緻fullGC。

如果是調用了 System.gc(); 方法所觸發的收集,那麼這将顯示

[GC (System.gc())

[Full GC (System.gc())

4.9.3 回收發生區域

[PSYoungGen

[ParOldGen

[Metaspace

表示GC發生的區域。這裡的命名是根據收集器而定的,Serial收集器新生代為為

DefNew(default new generation)

ParNew

收集器新生代為為

ParNew(parallel new generation)

,如果是

Parallel Scavenge

新生代為

PSYoungGen

老年代、元空間(或永久代)同理,名稱也是由收集器決定。

4.9.4 回收情況

收集區域後面的數字,例如:

[PSYoungGen: 2536K->504K(2560K)] 2939K->1101K(3584K)           

2536K->504K(2560K)

含義是

GC前該記憶體區域已使用的容量 -> GC 後該記憶體區域已使用的容量(該記憶體區域的總容量)

。而在方括号之外的

2939K->1101K(3584K)

則表示

GC前堆已使用容量 -> GC 後堆已使用容量(Java 堆的總容量)

4.9.5 回收耗時

在Java堆的總容量之後,是一個時間,表示該記憶體區域GC所占用的時間,機關是秒,例如

0.0018190

[PSYoungGen: 2536K->504K(2560K)] 2939K->1101K(3584K), 0.0018190 secs]           

有些收集器會給出更具體的時間資料:

[Times: user=0.00 sys=0.00, real=0.00 secs]           

這裡的user、sys、real和Linux的time指令所輸出的時間含義一緻,分别代表使用者态消耗的CPU時間、核心态消耗的CPU時間、操作從開始到結束所經過的牆鐘時間。

CPU時間與牆鐘時間的差別是,牆鐘時間包括各種非運算的等待耗時,例如等待磁盤I/O、等待線程堵塞,而CPU時間不包括這些耗時,但當系統有多CPU或者多核的話,多線程操作會疊加這些CPU時間,是以看到user或sys時間超過real時間完全是正常的。

5. 記憶體配置設定與回收政策

java對象的記憶體配置設定就是在堆上配置設定,對象主要配置設定在新生代的Eden區上,如果啟用了本地線程配置設定緩沖,将按線程優先在TLAB上配置設定,少數情況下也可能會直接配置設定在老年代中。配置設定的細節決定于目前使用的是哪種垃圾收集器組合,以及虛拟機中與記憶體相關的參數。

5.1 對象優先在Eden區配置設定

對象通常在新生代的Eden區進行配置設定,當Eden區沒有足夠空間進行配置設定時,虛拟機将發起一次Minor GC,與Minor GC對應的是Major GC、Full GC。

Minor GC:指發生在新生代的垃圾收集動作,非常頻繁,速度較快。

Major GC:指發生在老年代的GC,出現Major GC,經常會伴随一次Minor GC,同時Minor GC也會引起Major GC,一般在GC日志中統稱為GC,不頻繁。

Full GC:指發生在老年代和新生代的GC,速度很慢,需要Stop The World。           

檢視如下列印為例:

0.220: [GC (Allocation Failure) [PSYoungGen: 2400K->512K(4608K)] 16736K->15276K(19968K), 0.0011852 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.221: [Full GC (Ergonomics) [PSYoungGen: 512K->0K(4608K)] [ParOldGen: 14764K->15172K(15360K)] 15276K->15172K(19968K), [Metaspace: 3225K->3225K(1056768K)], 0.0062224 secs] [Times: user=0.00 sys=0.02, real=0.01 secs] 
0.228: [Full GC (Ergonomics) [PSYoungGen: 2048K->2048K(4608K)] [ParOldGen: 15172K->15172K(15360K)] 17220K->17220K(19968K), [Metaspace: 3226K->3226K(1056768K)], 0.0037570 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.232: [Full GC (Allocation Failure) [PSYoungGen: 2048K->2048K(4608K)] [ParOldGen: 15172K->15153K(15360K)] 17220K->17201K(19968K), [Metaspace: 3226K->3226K(1056768K)], 0.0080365 secs] [Times: user=0.08 sys=0.00, real=0.01 secs] 

Heap
 PSYoungGen      total 4608K, used 2171K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)
  eden space 4096K, 53% used [0x00000000ffb00000,0x00000000ffd1ed28,0x00000000fff00000)
  from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 15360K, used 15153K [0x00000000fec00000, 0x00000000ffb00000, 0x00000000ffb00000)
  object space 15360K, 98% used [0x00000000fec00000,0x00000000ffacc7b0,0x00000000ffb00000)
 Metaspace       used 3258K, capacity 4500K, committed 4864K, reserved 1056768K
  class space    used 353K, capacity 388K, committed 512K, reserved 1048576K           

可知初始配置設定記憶體都是在新生代,新生代配置設定失敗後啟動配置設定擔保到老年代。其中新生代Eden區和survivor區預設8:1。

5.2 大對象直接進入老年代

需要大量連續記憶體空間的Java對象稱為大對象,當沒有連續的空間容納大對象時,會提前觸發垃圾收集以擷取連續的空間來進行大對象的配置設定。虛拟機提供了

-XX:PretenureSizeThreadshold

參數來設定大對象的門檻值,超過門檻值的對象直接配置設定到老年代,這樣做的目的是為了避免在Eden區和兩個Survivor區之間發生大量的記憶體複制,因為新生代使用複制算法進行垃圾回收。

使用如下參數:

-verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:PretenureSizeThreshold=6M           
Heap
 PSYoungGen      total 9216K, used 2669K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 32% used [0x00000000ff600000,0x00000000ff89b698,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 6144K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 60% used [0x00000000fec00000,0x00000000ff200010,0x00000000ff600000)
 Metaspace       used 3230K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K
           

可以看到6m的大對象直接進入了老年代。

5.3 長期存活的對象進入老年代

虛拟機既然采用分代收集的思想來管理記憶體,那麼記憶體回收時就必須能夠識别哪些對象應當放在新生代,哪些對象應該放在老年代。為了做到這一點,虛拟機給每個對象定義了一個對象年齡計數器。如果對象在Eden出生并經過第一次Minor GC後仍然存活,并且能夠被Survivor容納的話,将被移動到Survivor空間中,并将對象年齡設定為1。

對象在Survivor區每熬過一次Minor GC,年齡就增加一歲,當它的年齡增加到一定程度(預設15歲)時,就會被晉升到老年代中。對象晉升老年代的年齡門檻值,可以通過參數

-XX:MaxTenuringThreshold

來設定。

例如使用如下參數:

-verbose:gc -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:MaxTenuringThreshold=1           
Heap
 PSYoungGen      total 9216K, used 2669K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 32% used [0x00000000ff600000,0x00000000ff89b698,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
  to   space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
 ParOldGen       total 10240K, used 6144K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 60% used [0x00000000fec00000,0x00000000ff200010,0x00000000ff600000)
 Metaspace       used 3234K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 350K, capacity 388K, committed 512K, reserved 1048576K           

可以看到1歲的對象直接被轉移到了老年代中。

5.4 動态對象年齡判斷

為了能更好地适應不同程式的記憶體狀況,虛拟機并不總是要求對象的年齡必須達到

MaxTenuringThreshold

才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大于survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到

MaxTenuringThreshold

中要求的年齡。

5.5 空間配置設定擔保

在發生Minor GC之前,虛拟機會檢查老年代連續的空閑區域是否大于新生代所有對象的總和,若成立,則說明Minor GC是安全的,否則,虛拟機需要檢視HandlePromotionFailure的值,看是否運作擔保失敗,若允許,則虛拟機繼續檢查老年代最大可用的連續空間是否大于曆次晉升到老年代對象的平均大小,如果大于,将嘗試進行一次Minor GC,盡管這次minor GC是有風險的;如果小于,或者HandlePromotionFailure設定不允許冒險,那麼這時也要改成一次Full GC。

在jdk1.6之後,HandlePromotionFailure的設定不會影響到虛拟機的空間配置設定擔保政策,隻要老年代的連續空間大于新生代對象總大小或者曆次晉升的平均大小就會進行Minor GC,否則将進行Full GC。

參考:

《深入了解java虛拟機》
https://wiki.openjdk.java.net/display/zgc/Main
https://blog.csdn.net/Leeycw96/article/details/90704760
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc.html#garbage_first_garbage_collection
https://blog.csdn.net/coderlius/article/details/79272773