第3章 垃圾收集器與內存分配策略
-
可達性分析算法
- 在Java技術體系里面,固定可作為GC Roots的對象包括以下幾種:
-
在虛擬機棧(棧幀中的本地變量表)中引用的對象,譬如各個線程被調用的方法堆棧中使用到的 參數、局部變量、臨時變量等。
-
在方法區中類靜態屬性引用的對象,譬如Java類的引用類型靜態變量。
-
在方法區中常量引用的對象,譬如字符串常量池(String Table)里的引用。
-
在本地方法棧中JNI(即通常所說的Native方法)引用的對象。
-
Java虛擬機內部的引用,如基本數據類型對應的Class對象,一些常駐的異常對象(比如 NullPointExcepiton、OutOfMemoryError)等,還有系統類加載器。 ·所有被同步鎖(synchronized關鍵字)持有的對象。
-
反映Java虛擬機內部情況的JMXBean、JVMTI中注冊的回調、本地代碼緩存等。
除了這些固定的GC Roots集合以外,根據用戶所選用的垃圾收集器以及當前回收的內存區域不 同,還可以有其他對象“臨時性”地加入,共同構成完整GC Roots集合。
- Java對引用的概念進行了擴充,將引用分為強引用(Strongly Re-ference)、軟 引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference)4種,這4種引用強 度依次逐漸減弱。
- 強引用是最傳統的“引用”的定義,是指在程序代碼之中普遍存在的引用賦值,即類似“Object obj=new Object()”這種引用關系。無論任何情況下,只要強引用關系還存在,垃圾收集器就永遠不會回 收掉被引用的對象。
- 軟引用是用來描述一些還有用,但非必須的對象。只被軟引用關聯着的對象,在系統將要發生內 存溢出異常前,會把這些對象列進回收范圍之中進行第二次回收,如果這次回收還沒有足夠的內存, 才會拋出內存溢出異常。在JDK 1.2版之后提供了SoftReference類來實現軟引用。
- 弱引用也是用來描述那些非必須對象,但是它的強度比軟引用更弱一些,被弱引用關聯的對象只 能生存到下一次垃圾收集發生為止。當垃圾收集器開始工作,無論當前內存是否足夠,都會回收掉只 被弱引用關聯的對象。在JDK 1.2版之后提供了WeakReference類來實現弱引用。
- 虛引用也稱為“幽靈引用”或者“幻影引用”,它是最弱的一種引用關系。一個對象是否有虛引用的 存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛 引用關聯的唯一目的只是為了能在這個對象被收集器回收時收到一個系統通知。在JDK 1.2版之后提供 了PhantomReference類來實現虛引用。
- 要回收一個對象,至少要經歷兩次標記過程:如果對象在進行可達性分析后發現沒 有與GC Roots相連接的引用鏈,那它將會被第一次標記,隨后進行一次篩選,篩選的條件是此對象是 否有必要執行finalize()方法。假如對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用 過,那么虛擬機將這兩種情況都視為“沒有必要執行”。
如果這個對象被判定為確有必要執行finalize()方法,那么該對象將會被放置在一個名為F-Queue的 隊列之中,並在稍后由一條由虛擬機自動建立的、低調度優先級的Finalizer線程去執行它們的finalize() 方法。這里所說的“執行”是指虛擬機會觸發這個方法開始運行,但並不承諾一定會等待它運行結束。
public class FinalizeEscapeGC { public static FinalizeEscapeGC SAVE_HOOK = null; public void isAlive() { System.out.println("yes, i am still alive :)"); } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize method executed!"); FinalizeEscapeGC.SAVE_HOOK = this; } public static void main(String[] args) throws Throwable { SAVE_HOOK = new FinalizeEscapeGC(); //對象第一次成功拯救自己 SAVE_HOOK = null; System.gc(); // 因為Finalizer方法優先級很低,暫停0.5秒,以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no, i am dead :("); } // 下面這段代碼與上面的完全相同,但是這次自救卻失敗了 SAVE_HOOK = null; System.gc(); // 因為Finalizer方法優先級很低,暫停0.5秒,以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no, i am dead :("); } } }
- 回收方法區
JDK 11時期的ZGC收集器就不支持類卸載
-
判定一個類型是否屬於“不再被使用的類”需要同時滿足下面三個條件:
該類所有的實例都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實例。
加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載器的場景,如 OSGi、JSP的重加載等,否則通常是很難達成的。
該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
Java虛擬機被允許對滿足上述三個條件的無用類進行回收,這里說的僅僅是“被允許”,而並不是 和對象一樣,沒有引用了就必然會回收。關於是否要對類型進行回收,HotSpot虛擬機提供了Xnoclassgc參數進行控制,還可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX: +TraceClassUnLoading查看類加載和卸載信息,其中-verbose:class和-XX:+TraceClassLoading可以在 Product版的虛擬機中使用,-XX:+TraceClassUnLoading參數需要FastDebug版[1]的虛擬機支持。
- 垃圾收集算法
從如何判定對象消亡的角度出發,垃圾收集算法可以划分為“引用計數式垃圾收集”(Reference Counting GC)和“追蹤式垃圾收集”(Tracing GC)兩大類,這兩類也常被稱作“直接垃圾收集”和“間接 垃圾收集”。
-
部分收集(Partial GC):指目標不是完整收集整個Java堆的垃圾收集,其中又分為:
新生代收集(Minor GC/Young GC):指目標只是新生代的垃圾收集。
老年代收集(Major GC/Old GC):指目標只是老年代的垃圾收集。目前只有CMS收集器會有單 獨收集老年代的行為。
混合收集(Mixed GC):指目標是收集整個新生代以及部分老年代的垃圾收集。目前只有G1收 集器會有這種行為。
-
整堆收集(Full GC):收集整個Java堆和方法區的垃圾收集。
-
標記-清除算法
-
標記-復制算法
-
標記-整理算法
HotSpot虛擬機里面關注吞吐量的Parallel Scavenge收集器是基於標記-整理算法的,而關注延遲的CMS收集器則是基於標記-清除算法的
讓虛擬機平時多數時間都采用標記-清除算法,暫時容忍內存碎片的存在,直到內存空間的碎片化程度已經 大到影響對象分配時,再采用標記-整理算法收集一次,以獲得規整的內存空間。CMS就是這樣做的
-
HotSpot的算法細節實現
-
根節點枚舉
所有收集器在根節點枚舉這一步驟時都是必須暫停用戶線程(Stop The World),包括CMS、G1、 ZGC等收集器
Java虛擬機使用的都是准確式垃圾收集,所以當用戶線程停頓下來之后,其實並不需要一個不漏地檢查完所有 執行上下文和全局的引用位置,虛擬機應當是有辦法直接得到哪些地方存放着對象引用的。在HotSpot 的解決方案里,是使用一組稱為OopMap的數據結構來達到這個目的。一旦類加載動作完成的時候, HotSpot就會把對象內什么偏移量上是什么類型的數據計算出來,在即時編譯(見第11章)過程中,也 會在特定的位置記錄下棧里和寄存器里哪些位置是引用。這樣收集器在掃描時就可以直接得知這些信 息了,並不需要真正一個不漏地從方法區等GC Roots開始查找。
-
安全點
OopMap的協助下,HotSpot可以快速准確地完成GC Roots枚舉。只是在“特定的位置”記錄 了這些信息,這些位置被稱為安全點(Safepoint)。
兩種中斷方案:搶先式中斷 (Preemptive Suspension)和主動式中斷(Voluntary Suspension),現在虛擬機都采用主動式中斷。
當垃圾收集需要中斷線程的時候,不直接對線程操作,僅僅簡單地設置一 個標志位,各個線程執行過程時會不停地主動去輪詢這個標志,一旦發現中斷標志為真時就自己在最 近的安全點上主動中斷掛起。輪詢標志的地方和安全點是重合的,另外還要加上所有創建對象和其他 需要在Java堆上分配內存的地方,這是為了檢查是否即將要發生垃圾收集,避免沒有足夠內存分配新 對象。
-
安全區域
用戶線程處於Sleep狀態或者Blocked狀態,這時候線程無法響應虛擬機的中斷請求,不能再走 到安全的地方去中斷掛起自己,虛擬機也顯然不可能持續等待線程重新被激活分配處理器時間。對於 這種情況,就必須引入安全區域(Safe Region)來解決。
當用戶線程執行到安全區域里面的代碼時,首先會標識自己已經進入了安全區域,那樣當這段時 間里虛擬機要發起垃圾收集時就不必去管這些已聲明自己在安全區域內的線程了。當線程要離開安全 區域時,它要檢查虛擬機是否已經完成了根節點枚舉(或者垃圾收集過程中其他需要暫停用戶線程的 階段),如果完成了,那線程就當作沒事發生過,繼續執行;否則它就必須一直等待,直到收到可以 離開安全區域的信號為止。
-
記憶集與卡表
所有涉及部分區域收集(Partial GC)行為的 垃圾收集器,典型的如G1、ZGC和Shenandoah收集器,為了避免把整個其他區域加進GC Roots掃描范圍,就需要記憶集(Remembered Set)的數據結構。
記憶集是一種用於記錄從非收集區域指向收集區域的指針集合的抽象數據結構。
實現方案:
- 字長精度:每個記錄精確到一個機器字長(就是處理器的尋址位數,如常見的32位或64位,這個 精度決定了機器訪問物理內存地址的指針長度),該字包含跨代指針。
- 對象精度:每個記錄精確到一個對象,該對象里有字段含有跨代指針。
- 卡精度:每個記錄精確到一塊內存區域,該區域內有對象含有跨代指針。
第三種“卡精度”所指的是用一種稱為“卡表”(Card Table)的方式去實現記憶集
卡表最簡單的形式可以只是一個字節數組[2],而HotSpot虛擬機確實也是這樣做的。以下這行代 碼是HotSpot默認的卡表標記邏輯
CARD_TABLE [this address >> 9] = 0;
字節數組CARD_TABLE的每一個元素都對應着其標識的內存區域中一塊特定大小的內存塊,這個 內存塊被稱作“卡頁”(Card Page)。一般來說,卡頁大小都是以2的N次冪的字節數,通過上面代碼可 以看出HotSpot中使用的卡頁是2的9次冪,即512字節(地址右移9位,相當於用地址除以512)。那如 果卡表標識內存區域的起始地址是0x0000的話,數組CARD_TABLE的第0、1、2號元素,分別對應了 地址范圍為0x0000~0x01FF、0x0200~0x03FF、0x0400~0x05FF的卡頁內存塊.
一個卡頁的內存中通常包含不止一個對象,只要卡頁內有一個(或更多)對象的字段存在着跨代 指針,那就將對應卡表的數組元素的值標識為1,稱為這個元素變臟(Dirty),沒有則標識為0。在垃 圾收集發生時,只要篩選出卡表中變臟的元素,就能輕易得出哪些卡頁內存塊中包含跨代指針,把它 們加入GC Roots中一並掃描。
-
-
寫屏障
在HotSpot虛擬機里是通過寫屏障(Write Barrier)技術維護卡表狀態的。類似AOP
寫屏障的開銷外,卡表在高並發場景下還面臨着“偽共享”(False Sharing)問題.也就是緩存行。
在JDK 7之后,HotSpot虛擬機增加了一個新的參數-XX:+UseCondCardMark,用來決定是否開啟 卡表更新的條件判斷。開啟會增加一次額外判斷的開銷,但能夠避免偽共享問題,兩者各有性能損 耗,是否打開要根據應用實際運行情況來進行測試權衡。
-
並發的可達性分析
三色標記
- 白色:表示對象尚未被垃圾收集器訪問過。顯然在可達性分析剛剛開始的階段,所有的對象都是 白色的,若在分析結束的階段,仍然是白色的對象,即代表不可達。
- 黑色:表示對象已經被垃圾收集器訪問過,且這個對象的所有引用都已經掃描過。黑色的對象代 表已經掃描過,它是安全存活的,如果有其他對象引用指向了黑色對象,無須重新掃描一遍。黑色對 象不可能直接(不經過灰色對象)指向某個白色對象。
- 灰色:表示對象已經被垃圾收集器訪問過,但這個對象上至少存在一個引用還沒有被掃描過。
-
經典垃圾收集器
圖中展示了七種作用於不同分代的收集器,如果兩個收集器之間存在連線,就說明它們可以搭配 使用[3],圖中收集器所處的區域,則表示它是屬於新生代收集器抑或是老年代收集器。
在JDK 8時將Serial+CMS、 ParNew+Serial Old這兩個組合聲明為廢棄,並在JDK 9中完全取消了這些組合的支持
-
Serial收集器
單線程工作的收集器,強 調在它進行垃圾收集時,必須暫停其他所有工作線程,直到它收集結束。
Serial 采用復制算法,Serial Old采用標記整理算法
-
ParNew收集器
實質上是Serial收集器的多線程並行版本,除了同時使用多條線程進行垃圾收集之 外,其余的行為包括Serial收集器可用的所有控制參數、收集算法、Stop The World、對象分配規 則、回收策略等都與Serial收集器完全一致,在實現上這兩種收集器也共用了相當多的代碼。
除了Serial收集器外,目前只有它能與CMS 收集器配合工作。
涉及“並發”和“並行”概念的收集器,並發,並行的概念
並行(Parallel):並行描述的是多條垃圾收集器線程之間的關系,說明同一時間有多條這樣的線 程在協同工作,通常默認此時用戶線程是處於等待狀態。
並發(Concurrent):並發描述的是垃圾收集器線程與用戶線程之間的關系,說明同一時間垃圾 收集器線程與用戶線程都在運行。由
CMS作為老年代的收集器,卻無法與JDK 1.4.0中已經存在的新生代收集器Parallel Scavenge配合工作的原因:
-
一個面向低延遲一個面向高吞吐量的目標不一致
-
技術上的原因是Parallel Scavenge收集器及 后面提到的G1收集器等都沒有使用HotSpot中原本設計的垃圾收集器的分代框架,而選擇另外獨立實 現。
-
Parallel Scavenge收集器
一款新生代收集器,也是基於標記-復制算法實現的收集器。
Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是盡可能 地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐 量(Throughput)。
-
Serial Old收集器
Serial收集器的老年代版本。
與Parallel Scavenge收集器搭配使用,也可以作為CMS 收集器發生失敗時的后備預案,在並發收集發生Concurrent Mode Failure時使用。
Parallel Scavenge收集器架構中本身有PS MarkSweep收集器來進行老年代收集,並非 直接調用Serial Old收集器,但是這個PS MarkSweep收集器與Serial Old的實現幾乎是一樣的,所以在官 方的許多資料中都是直接以Serial Old代替PS MarkSweep進行講解。
-
Parallel Old收集器
Parallel Scavenge收集器的老年代版本,支持多線程並發收集,基於標記-整理算法實 現。
關注吞吐量或處理器資源稀缺時,可以考慮Parallel Scavenge加Parallel Old收集器這個組合。
-
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器,基於標記-清除算法實現。
整個過程分為四個步驟,包括:
1)初始標記(CMS initial mark)
2)並發標記(CMS concurrent mark)
3)重新標記(CMS remark)
4)並發清除(CMS concurrent sweep)
缺點:
1)CMS收集器對處理器資源非常敏感。
2)無法處理“浮動垃圾”(Floating Garbage),有可能出現“Con-current Mode Failure”失敗進而導致另一次完全“Stop The World”的Full GC的產生。
3)CMS是一款基於“標記-清除”算法實現的收集器,收集結束時會有大量空間碎片產生。有觸發觸發Full GC的情況。
-
Garbage First收集器
把連續的Java堆划分為多個大小相等的獨立區域(Region),每一個Region都可以 根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。收集器能夠對扮演不同角色的 Region采用不同的策略去處理,這樣無論是新創建的對象還是已經存活了一段時間、熬過多次收集的 舊對象都能獲取很好的收集效果。Region中還有一類特殊的Humongous區域,專門用來存儲大對象。G1認為只要大小超過了一個 Region容量一半的對象即可判定為大對象。
G1收集器之所以能建立可預測的停頓時間模型,是因為它將Region作 為單次回收的最小單元,即每次收集到的內存空間都是Region大小的整數倍,這樣可以有計划地避免 在整個Java堆中進行全區域的垃圾收集。
內存回收的速度趕不上內存分配的速度, G1收集器也要被迫凍結用戶線程執行,導致Full GC而產生長時間“Stop The World”。
G1收集器的 運作過程大致可划分為以下四個步驟:
1)初始標記(Initial Marking):僅僅只是標記一下GC Roots能直接關聯到的對象,並且修改TAMS 指針的值,讓下一階段用戶線程並發運行時,能正確地在可用的Region中分配新對象。這個階段需要 停頓線程,但耗時很短,而且是借用進行Minor GC的時候同步完成的,所以G1收集器在這個階段實際 並沒有額外的停頓。
2)並發標記(Concurrent Marking):從GC Root開始對堆中對象進行可達性分析,遞歸掃描整個堆 里的對象圖,找出要回收的對象,這階段耗時較長,但可與用戶程序並發執行。當對象圖掃描完成以 后,還要重新處理SATB記錄下的在並發時有引用變動的對象。
3)最終標記(Final Marking):對用戶線程做另一個短暫的暫停,用於處理並發階段結束后仍遺留 下來的最后那少量的SATB記錄。
4)篩選回收(Live Data Counting and Evacuation):負責更新Region的統計數據,對各個Region的回 收價值和成本進行排序,根據用戶所期望的停頓時間來制定回收計划,可以自由選擇任意多個Region 構成回收集,然后把決定回收的那一部分Region的存活對象復制到空的Region中,再清理掉整個舊 Region的全部空間。這里的操作涉及存活對象的移動,是必須暫停用戶線程,由多條收集器線程並行 完成的。
G1從整體來看是基於“標記-整理”算法實現的收集器,但從局部(兩個Region 之間)上看又是基於“標記-復制”算法實現。這種特性有利於程序長時間運行,在程序為大 對象分配內存時不容易因無法找到連續內存空間而提前觸發下一次收集。
缺點:
在用戶程序運行過程 中,G1無論是為了垃圾收集產生的內存占用(Footprint)還是程序運行時的額外執行負載 (Overload)都要比CMS要高。
-
-
低延遲垃圾收集器
衡量垃圾收集器的三項最重要的指標是:內存占用(Footprint)、吞吐量(Throughput)和延遲 (Latency),三者共同構成了一個“不可能三角[1]”。
Shenandoah和ZGC,幾乎整個工作過程全 部都是並發的,只有初始標記、最終標記這些階段有短暫的停頓,這部分停頓的時間基本上是固定 的,與堆的容量、堆中對象的數量沒有正比例關系。
-
Shenandoah收集器
2014年RedHat把Shenandoah貢獻 給了OpenJDK(第一款不由Oracle(包括以前的Sun)公 司的虛擬機團隊所領導開發的HotSpot垃圾收集器).
目標 是實現一種能在任何堆內存大小下都可以把垃圾收集的停頓時間限制在十毫秒以內的垃圾收集器
Shenandoah摒棄了在G1中耗費大量內存和計算資源去維護的記憶集,改用名為“連接矩陣”(Connection Matrix)的全局數據結構來記錄跨Region的引用關系,降低了處理跨代指針時的記憶集維護消耗,也降 低了偽共享問題(見3.4.4節)的發生概率。
Shenandoah收集器的工作過程大致可以划分為以下九個階段:
1)·初始標記(Initial Marking):與G1一樣,首先標記與GC Roots直接關聯的對象,這個階段仍 是“Stop The World”的,但停頓時間與堆大小無關,只與GC Roots的數量相關。
2)並發標記(Concurrent Marking):與G1一樣,遍歷對象圖,標記出全部可達的對象,這個階段 是與用戶線程一起並發的,時間長短取決於堆中存活對象的數量以及對象圖的結構復雜程度。
3)最終標記(Final Marking):與G1一樣,處理剩余的SATB掃描,並在這個階段統計出回收價值 最高的Region,將這些Region構成一組回收集(Collection Set)。最終標記階段也會有一小段短暫的停 頓。
4)並發清理(Concurrent Cleanup):這個階段用於清理那些整個區域內連一個存活對象都沒有找到 的Region(這類Region被稱為Immediate Garbage Region)。
5)並發回收(Concurrent Evacuation):並發回收階段是Shenandoah與之前HotSpot中其他收集器的 核心差異。在這個階段,Shenandoah要把回收集里面的存活對象先復制一份到其他未被使用的Region之 中。復制對象這件事情如果將用戶線程凍結起來再做那是相當簡單的,但如果兩者必須要同時並發進 行的話,就變得復雜起來了。其困難點是在移動對象的同時,用戶線程仍然可能不停對被移動的對象 進行讀寫訪問,移動對象是一次性的行為,但移動之后整個內存中所有指向該對象的引用都還是舊對 象的地址,這是很難一瞬間全部改變過來的。對於並發回收階段遇到的這些困難,Shenandoah將會通 過讀屏障和被稱為“Brooks Pointers”的轉發指針來解決(講解完Shenandoah整個工作過程之后筆者還要 再回頭介紹它)。並發回收階段運行的時間長短取決於回收集的大小。
6)初始引用更新(Initial Update Reference):並發回收階段復制對象結束后,還需要把堆中所有指 向舊對象的引用修正到復制后的新地址,這個操作稱為引用更新。引用更新的初始化階段實際上並未 做什么具體的處理,設立這個階段只是為了建立一個線程集合點,確保所有並發回收階段中進行的收 集器線程都已完成分配給它們的對象移動任務而已。初始引用更新時間很短,會產生一個非常短暫的 停頓。 7)
並發引用更新(Concurrent Update Reference):真正開始進行引用更新操作,這個階段是與用戶 線程一起並發的,時間長短取決於內存中涉及的引用數量的多少。並發引用更新與並發標記不同,它 不再需要沿着對象圖來搜索,只需要按照內存物理地址的順序,線性地搜索出引用類型,把舊值改為 新值即可。
8)最終引用更新(Final Update Reference):解決了堆中的引用更新后,還要修正存在於GC Roots 中的引用。這個階段是Shenandoah的最后一次停頓,停頓時間只與GC Roots的數量相關。
9)並發清理(Concurrent Cleanup):經過並發回收和引用更新之后,整個回收集中所有的Region已 再無存活對象,這些Region都變成Immediate Garbage Regions了,最后再調用一次並發清理過程來回收 這些Region的內存空間,供以后新對象分配使用。
三個最重要的並發階段(並發標記、並發回收、並發引用更新)
-
ZGC收集器
ZGC的內存布局。與Shenandoah和G1一樣,ZGC也采用基於Region的堆內存布局,但與它們不同的是,ZGC的Region(在一些官方資料中將它稱為Page或者ZPage,本章為行文一致繼續稱 為Region)具有動態性——動態創建和銷毀,以及動態的區域容量大小。在x64硬件平台下,ZGC的 Region可以具有如圖3-19所示的大、中、小三類容量:
小型Region(Small Region):容量固定為2MB,用於放置小於256KB的小對象。
中型Region(Medium Region):容量固定為32MB,用於放置大於等於256KB但小於4MB的對 象。
大型Region(Large Region):容量不固定,可以動態變化,但必須為2MB的整數倍,用於放置 4MB或以上的大對象。
ZGC的運作過程大致可划分為以下四個大的階 段。全部四個階段都是可以並發執行的,僅是兩個階段中間會存在短暫的停頓小階段:
1)並發標記(Concurrent Mark):與G1、Shenandoah一樣,並發標記是遍歷對象圖做可達性分析的 階段,前后也要經過類似於G1、Shenandoah的初始標記、最終標記(盡管ZGC中的名字不叫這些)的 短暫停頓,而且這些停頓階段所做的事情在目標上也是相類似的。與G1、Shenandoah不同的是,ZGC 的標記是在指針上而不是在對象上進行的,標記階段會更新染色指針中的Marked 0、Marked 1標志 位。
2)並發預備重分配(Concurrent Prepare for Relocate):這個階段需要根據特定的查詢條件統計得出 本次收集過程要清理哪些Region,將這些Region組成重分配集(Relocation Set)。重分配集與G1收集器 的回收集(Collection Set)還是有區別的,ZGC划分Region的目的並非為了像G1那樣做收益優先的增 量回收。相反,ZGC每次回收都會掃描所有的Region,用范圍更大的掃描成本換取省去G1中記憶集的 維護成本。因此,ZGC的重分配集只是決定了里面的存活對象會被重新復制到其他的Region中,里面 的Region會被釋放,而並不能說回收行為就只是針對這個集合里面的Region進行,因為標記過程是針對 全堆的。此外,在JDK 12的ZGC中開始支持的類卸載以及弱引用的處理,也是在這個階段中完成的。
3)並發重分配(Concurrent Relocate):重分配是ZGC執行過程中的核心階段,這個過程要把重分 配集中的存活對象復制到新的Region上,並為重分配集中的每個Region維護一個轉發表(Forward Table),記錄從舊對象到新對象的轉向關系。得益於染色指針的支持,ZGC收集器能僅從引用上就明 確得知一個對象是否處於重分配集之中,如果用戶線程此時並發訪問了位於重分配集中的對象,這次 訪問將會被預置的內存屏障所截獲,然后立即根據Region上的轉發表記錄將訪問轉發到新復制的對象 上,並同時修正更新該引用的值,使其直接指向新對象,ZGC將這種行為稱為指針的“自愈”(SelfHealing)能力。這樣做的好處是只有第一次訪問舊對象會陷入轉發,也就是只慢一次,對比 Shenandoah的Brooks轉發指針,那是每次對象訪問都必須付出的固定開銷,簡單地說就是每次都慢, 因此ZGC對用戶程序的運行時負載要比Shenandoah來得更低一些。還有另外一個直接的好處是由於染 色指針的存在,一旦重分配集中某個Region的存活對象都復制完畢后,這個Region就可以立即釋放用於 新對象的分配(但是轉發表還得留着不能釋放掉),哪怕堆中還有很多指向這個對象的未更新指針也 沒有關系,這些舊指針一旦被使用,它們都是可以自愈的。
4)並發重映射(Concurrent Remap):重映射所做的就是修正整個堆中指向重分配集中舊對象的所 有引用,這一點從目標角度看是與Shenandoah並發引用更新階段一樣的,但是ZGC的並發重映射並不 是一個必須要“迫切”去完成的任務,因為前面說過,即使是舊引用,它也是可以自愈的,最多只是第 一次使用時多一次轉發和修正操作。重映射清理這些舊引用的主要目的是為了不變慢(還有清理結束 后可以釋放轉發表這樣的附帶收益),所以說這並不是很“迫切”。因此,ZGC很巧妙地把並發重映射 階段要做的工作,合並到了下一次垃圾收集循環中的並發標記階段里去完成,反正它們都是要遍歷所 有對象的,這樣合並就節省了一次遍歷對象圖[9]的開銷。一旦所有指針都被修正之后,原來記錄新舊 對象關系的轉發表就可以釋放掉了。
低延遲為首要目標的ZGC已經達到了以高吞吐量為目標Parallel Scavenge 的99%,直接超越了G1。如果將吞吐量測試設定為面向SLA(Service Level Agreements)應用 的“Critical Throughput”的話[16],ZGC的表現甚至還反超了Parallel Scavenge收集器。
-
虛擬機及垃圾收集器日志
在JDK 9以前,HotSpot並沒 有提供統一的日志處理框架,虛擬機各個功能模塊的日志開關分布在不同的參數上,日志級別、循環 日志大小、輸出格式、重定向等設置在不同功能上都要單獨解決。
JDK 9開始,HotSpot所有功能的日志都收歸到了“-Xlog”參數上
-Xlog[:[selector][:[output][:[decorators][:output-options]]]]
命令行中最關鍵的參數是選擇器(Selector),它由標簽(Tag)和日志級別(Level)共同組成。 標簽可理解為虛擬機中某個功能模塊的名字,它告訴日志框架用戶希望得到虛擬機哪些功能的日志輸 出。垃圾收集器的標簽名稱為“gc”,由此可見,垃圾收集器日志只是HotSpot眾多功能日志的其中一 項,全部支持的功能模塊標簽名如下所示:
!
日志級別從低到高,共有Trace,Debug,Info,Warning,Error,Off六種級別,日志級別決定了輸 出信息的詳細程度,默認級別為Info,HotSpot的日志規則與Log4j、SLF4j這類Java日志框架大體上是 一致的。另外,還可以使用修飾器(Decorator)來要求每行日志輸出都附加上額外的內容,支持附加 在日志行上的信息包括:
·time:當前日期和時間。
·uptime:虛擬機啟動到現在經過的時間,以秒為單位。
·timemillis:當前時間的毫秒數,相當於System.currentTimeMillis()的輸出。
·uptimemillis:虛擬機啟動到現在經過的毫秒數。
·timenanos:當前時間的納秒數,相當於System.nanoTime()的輸出。
·uptimenanos:虛擬機啟動到現在經過的納秒數。
·pid:進程ID。
·tid:線程ID。
·level:日志級別。
·tags:日志輸出的標簽集。
-
JDK 9統一日志框架前、后是如何獲得垃圾收集器過程的相關信 息
1)查看GC基本信息,在JDK 9之前使用-XX:+PrintGC,JDK 9后使用-Xlog:gc:
2)查看GC詳細信息,在JDK 9之前使用-XX:+PrintGCDetails,在JDK 9之后使用-X-log:gc, 用通配符將GC標簽下所有細分過程都打印出來,如果把日志級別調整到Debug或者Trace,還將獲得更多細節信息:
3)查看GC前后的堆、方法區可用容量變化,在JDK 9之前使用-XX:+PrintHeapAtGC,JDK 9之 后使用-Xlog:gc+heap=debug:
4)查看GC過程中用戶線程並發時間以及停頓的時間,在JDK 9之前使用-XX:+PrintGCApplicationConcurrentTime以及-XX:+PrintGCApplicationStoppedTime,JDK 9之后使用-Xlog: safepoint:
5)查看收集器Ergonomics機制(自動設置堆空間各分代區域大小、收集目標等內容,從Parallel收 集器開始支持)自動調節的相關信息。在JDK 9之前使用-XX:+PrintAdaptive-SizePolicy,JDK 9之后 使用-Xlog:gc+ergo*=trace:
6)查看熬過收集后剩余對象的年齡分布信息,在JDK 9前使用-XX:+PrintTenuring-Distribution, JDK 9之后使用-Xlog:gc+age=trace:
!
-
垃圾收集器參數總結
垃圾收集相關的常用參數