《深入理解java虛擬機器》第3版筆記3

wang03發表於2021-08-28

第3章 垃圾收集器與記憶體分配策略

  1. 可達性分析演算法

    1. 在Java技術體系裡面,固定可作為GC Roots的物件包括以下幾種:
    • 在虛擬機器棧(棧幀中的本地變數表)中引用的物件,譬如各個執行緒被呼叫的方法堆疊中使用到的 引數、區域性變數、臨時變數等。

    • 在方法區中類靜態屬性引用的物件,譬如Java類的引用型別靜態變數。

    • 在方法區中常量引用的物件,譬如字串常量池(String Table)裡的引用。

    • 在本地方法棧中JNI(即通常所說的Native方法)引用的物件。

    • Java虛擬機器內部的引用,如基本資料型別對應的Class物件,一些常駐的異常物件(比如 NullPointExcepiton、OutOfMemoryError)等,還有系統類載入器。 ·所有被同步鎖(synchronized關鍵字)持有的物件。

    • 反映Java虛擬機器內部情況的JMXBean、JVMTI中註冊的回撥、原生程式碼快取等。

      除了這些固定的GC Roots集合以外,根據使用者所選用的垃圾收集器以及當前回收的記憶體區域不 同,還可以有其他物件“臨時性”地加入,共同構成完整GC Roots集合。

    1. 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類來實現虛引用。
    1. 要回收一個物件,至少要經歷兩次標記過程:如果物件在進行可達性分析後發現沒 有與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 :(");
            }
        }
    }
    
    
    1. 回收方法區

    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]的虛擬機器支援。

    1. 垃圾收集演算法

    從如何判定物件消亡的角度出發,垃圾收集演算法可以劃分為“引用計數式垃圾收集”(Reference Counting GC)和“追蹤式垃圾收集”(Tracing GC)兩大類,這兩類也常被稱作“直接垃圾收集”和“間接 垃圾收集”。

    • 部分收集(Partial GC):指目標不是完整收集整個Java堆的垃圾收集,其中又分為:

      新生代收集(Minor GC/Young GC):指目標只是新生代的垃圾收集。

      老年代收集(Major GC/Old GC):指目標只是老年代的垃圾收集。目前只有CMS收集器會有單 獨收集老年代的行為。

      混合收集(Mixed GC):指目標是收集整個新生代以及部分老年代的垃圾收集。目前只有G1收 集器會有這種行為。

    • 整堆收集(Full GC):收集整個Java堆和方法區的垃圾收集。

    1. 標記-清除演算法

    2. 標記-複製演算法

    3. 標記-整理演算法

      HotSpot虛擬機器裡面關注吞吐量的Parallel Scavenge收集器是基於標記-整理演算法的,而關注延遲的CMS收集器則是基於標記-清除演算法的

      讓虛擬機器平時多數時間都採用標記-清除演算法,暫時容忍記憶體碎片的存在,直到記憶體空間的碎片化程度已經 大到影響物件分配時,再採用標記-整理演算法收集一次,以獲得規整的記憶體空間。CMS就是這樣做的

    4. 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中一併掃描。

    5. 寫屏障

      在HotSpot虛擬機器裡是通過寫屏障(Write Barrier)技術維護卡表狀態的。類似AOP

      寫屏障的開銷外,卡表在高併發場景下還面臨著“偽共享”(False Sharing)問題.也就是快取行。

      在JDK 7之後,HotSpot虛擬機器增加了一個新的引數-XX:+UseCondCardMark,用來決定是否開啟 卡表更新的條件判斷。開啟會增加一次額外判斷的開銷,但能夠避免偽共享問題,兩者各有效能損 耗,是否開啟要根據應用實際執行情況來進行測試權衡。

    6. 併發的可達性分析

      三色標記

      • 白色:表示物件尚未被垃圾收集器訪問過。顯然在可達性分析剛剛開始的階段,所有的物件都是 白色的,若在分析結束的階段,仍然是白色的物件,即代表不可達。
      • 黑色:表示物件已經被垃圾收集器訪問過,且這個物件的所有引用都已經掃描過。黑色的物件代 表已經掃描過,它是安全存活的,如果有其他物件引用指向了黑色物件,無須重新掃描一遍。黑色對 象不可能直接(不經過灰色物件)指向某個白色物件。
      • 灰色:表示物件已經被垃圾收集器訪問過,但這個物件上至少存在一個引用還沒有被掃描過。
    7. 經典垃圾收集器

      圖中展示了七種作用於不同分代的收集器,如果兩個收集器之間存在連線,就說明它們可以搭配 使用[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要高。

  2. 低延遲垃圾收集器

    衡量垃圾收集器的三項最重要的指標是:記憶體佔用(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的記憶體空間,供以後新物件分配使用。

    三個最重要的併發階段(併發標記、併發回收、併發引用更新)

  1. 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收集器。

  2. 虛擬機器及垃圾收集器日誌

    在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:

    !

  1. 垃圾收集器引數總結

    垃圾收集相關的常用引數

相關文章