JVM學習--GC&&MAP

概述

垃圾收集(GC)

  1. 哪些內存需要回收
  2. 什麼時候回收
  3. 如何回收

垃圾收集主要關注的是Java堆和方法區

  程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生,隨線程而滅,棧中的棧幀隨着方法的進入和退出而有條不紊地執行着出棧和入棧操作。故垃圾回收不需要考慮這些

回收

回收JAVA堆

引用計數算法

	在對象中添加一個引用計數器,每當有一個地方引用的時候,計數器數值加一;當引用失效,計數器值則減一;	
	需要配合大量的額外處理才能保證正確的工作
  • 可達性分析算法
      這個算法的基本思路就是通過一系列稱爲“GC Roots”的根對象作爲起始節點集,從這些節點開始,根據引用關係向下搜索,搜索過程所走過的路徑稱爲“引用鏈”(Reference Chain),如果某個對象到GC Roots間沒有任何引用鏈相連,或者用圖論的話來說就是從GC Roots到這個對象不可達時,則證明此對象是不可能再被使用的。
    在這裏插入圖片描述

在Java技術體系裏面,固定可作爲GC Roots的對象包括以下幾種:
·在虛擬機棧(棧幀中的本地變量表)中引用的對象,譬如各個線程被調用的方法堆棧中使用到的參數、局部變量、臨時變量等。
·在方法區中類靜態屬性引用的對象,譬如Java類的引用類型靜態變量。
·在方法區中常量引用的對象,譬如字符串常量池(String Table)裏的引用。
·在本地方法棧中JNI(即通常所說的Native方法)引用的對象。
·Java虛擬機內部的引用,如基本數據類型對應的Class對象,一些常駐的異常對象(比如NullPointExcepiton、OutOfMemoryError)等,還有系統類加載器。
·所有被同步鎖(synchronized關鍵字)持有的對象。
·反映Java虛擬機內部情況的JMXBean、JVMTI中註冊的回調、本地代碼緩存等

引用

在JDK 1.2版之前,Java裏面的引用是很傳統的定義:
  如果reference類型的數據中存儲的數值代表的是另外一塊內存的起始地址,就稱該reference數據是代表某塊內存、某個對象的引用。

  • 強引用

在程序代碼之中普遍存在的引用賦值,即類似“Object
obj=new Object()”這種引用關係。
只要強引用關係還存在,GC就永遠不會回收被引用的對象

  • 軟引用

用來描述一些還有用,但非必須的對象。
只被軟引用關聯着的對象,在系統將要發生內存溢出異常前,會把這些對象列進回收範圍之中進行第二次回收,如果這次回收還沒有足夠的內存,纔會拋出內存溢出異常。

  • 弱引用

強度比軟引用更弱
被弱引用關聯的對象只能生存到下一次的垃圾收集發生爲止

  • 虛引用

也稱爲“幽靈引用”或者“幻影引用”,它是最弱的一種引用關係。
一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。
設置虛引用關聯的唯一目的只是爲了能在這個對象被垃圾收集器回收的時收到一個系統通知

Die Or Live

  在可達性分析的算法中即使判定爲不可達對象,也不是"非死不可"的;
真正宣告一個對象的死亡,至少經歷倆次標記
1. 如果對象在進行可達性分析後沒有與GC Roots相連接的引用鏈,則會進行標記,隨後進行篩選
2. 判斷是否需要執行finalize()方法

若對象沒有覆蓋finalize(),或者finalize()方法已經被虛擬機調用過,則沒有必要執行死刑

若確定執行“死刑”

該對象會被放置在一個名爲F-Queue的隊列中
由一條由虛擬機自動建立的、低調度優先級的Finalizer程去執行它們的finalize()方法。

注意,即使是死刑也可以被拯救

虛擬機會觸發finalize()方法,但是並不承諾一定會等待它運行結束
若某個對象不想死了,它的finalize()方法執行緩慢,或者是發生死循環,導致F-Queue隊列隊列中其他的對象永久處於等待狀態,甚至導致整個內存回收子系統崩潰
finalize()方法是對象逃脫死亡的最後一次機會,稍後收集器會對F-Queue中的對象進行第二次小規模的標記,如果對象重新與引用鏈上的任何一個對象建立關聯即可逃離

回收方法區

方法區垃圾收集的“性價比”通常是比較低的;
  在Java堆中,尤其是在新生代中,對常規應用進行一次垃圾收集通常可以回收70%至99%的內存空間,相比之下,方法區回收囿於苛刻的判定條件,其區域垃圾收集的回收成果往往遠低於此。
方法區的GC主要回收倆部分:廢棄的常量不再使用的類型

回收廢棄的常量與回收Java堆中的對象非常類似

判斷一個類型是否屬於“ 不再被使用的類

  • 該類的所有實例都已經被回收,即Java堆中不存在該類及其任何派生子類的實例
  • 加載該類的類加載器已經被回收(這個條件很難實現)
  • 該類對應的java.lang.Class對象沒有在任何地方被引用,無法通過反射訪問

Java虛擬機被允許對滿足上述三個條件的無用類進行回收,這裏說的僅僅是“被允許”,而並不是和對象一樣,沒有引用了就必然會回收。關於是否要對類型進行回收,HotSpot虛擬機提供了-Xnoclassgc參數進行控制,還可以使用-verbose:class以及-XX:+TraceClass-Loading、
-XX+TraceClassUnLoading查看類加載和卸載信息,其中-verbose:class和-XX:+TraceClassLoading可以在
Product版的虛擬機中使用,-XX:+TraceClassUnLoading參數需要FastDebug版[1]的虛擬機支持。

垃圾收集算法

引用計數器式垃圾收集(CRGC)

也叫直接垃圾收集

追蹤式垃圾收集(TGC)

也叫間接垃圾收集

分代收集

  • 弱分代假說
    大部分都是朝生夕滅
  • 強分代假說
    越戰越強

   這兩個分代假說共同奠定了多款常用的垃圾收集器的一致的設計原則:收集器應該將Java堆劃分出不同的區域,然後將回收對象依據其年齡(年齡即對象熬過垃圾收集過程的次數)分配到不同的區域之中存儲。顯而易見,如果一個區域中大多數對象都是朝生夕滅,難以熬過垃圾收集過程的話,那麼把它們集中放在一起,每次回收時只關注如何保留少量存活而不是去標記那些大量將要被回收的對象,就能以較低代價回收到大量的空間;如果剩下的都是難以消亡的對象,那把它們集中放在一塊,虛擬機便可以使用較低的頻率來回收這個區域,這就同時兼顧了垃圾收集的時間開銷和內存的空間有效利用。

  • 跨代引用假說
    存在互相引用關係的兩個對象,是應該傾向於同時生存或者同時消亡的。

標記-清除算法

算法思想:

  首先標記出所有需要回收的對象,在標記完成後,統一回收掉所有被標記的對象,也可以反過來,標記存活的對象,統一回收所有未被標記的對象。

  標記的過程就是判斷對象是否屬於垃圾的判定過程
缺點:
1. 執行效率不穩定
2. 內存空間的碎片化
執行過程
在這裏插入圖片描述

標記-複製算法

爲了解決標記-清除算法的執行效率低的問題而提出的
算法思想:

  將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。如果內存中多數對象都是存活的,這種算法將會產生大量的內存間複製的開銷,但對於多數對象都是可回收的情況,算法需要複製的就是佔少數的存活對象,而且每次都是針對整個半區進行內存回收,分配內存時也就不用考慮有空間碎片的複雜情況,只要移動堆頂指針,按順序分配即可。

缺點:

  1. 將可用內存縮小爲原來的一半
  2. 在對象存活率較高時就要進行較多的複製操作,效率將會降低

執行過程
在這裏插入圖片描述

標記-清理

針對老年代對象的存亡特徵提出的
算法思想:

  標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向內存空間一端移動,然後直接清理掉邊界以外的內存

執行過程
在這裏插入圖片描述

HotSpot的算法細節

根節點枚舉

背景:
  所有收集器在根節點枚舉這一步驟時都是必須暫停用戶線程的,因此毫無疑問根節點枚舉與之前提及的整理內存碎片一樣會面臨相似的“Stop The World”的困擾。

  當用戶線程停頓下來之後,其實並不需要一個不漏地檢查完所有執行上下文和全局的引用位置,虛擬機應當是有辦法直接得到哪些地方存放着對象引用的。

解決方案:
  HotSpot使用一組稱爲OopMap的數據結構來達到這個目的。一旦類加載動作完成的時候,HotSpot就會把對象內什麼偏移量上是什麼類型的數據計算出來,在即時編譯過程中,也會在特定的位置記錄下棧裏和寄存器裏哪些位置是引用。這樣收集器在掃描時就可以直接得知這些信息了,並不需要真正一個不漏地從方法區等GC Roots開始查找。

安全點

問題:
  導致OopMap內容變化的指令非常多,若爲每一條指令產生一個OopMap,那將會需要大量的額外空間
安全點:
  HotSpot只在“特定的位置”記錄了這些信息,這些位置稱爲安全點
  安全點的設定決定用戶程序執行時並非在代碼指令流的任意位置都能停頓下來進行垃圾收集,而是強制要求必須執行到達安全點以後才能暫停
安全點的選擇原則:

  • 不能讓收集器等待時間過長
  • 不能太過頻繁,否則會過分增大運行時的內存負荷
  • 以“是否具有讓程序長時間執行的特徵”爲標準進行選定

“ 長時間執行”的最明顯的特徵就是指令序列的複用
例如:方法調用、循環跳轉、異常跳轉等都屬於指令序列的複用
只有具備長時間執行功能的指令纔會產生安全點

如何在GC的時候讓所有的線程都跑到最近的安全點?

  • 搶斷式中斷
      不需要線程的執行代碼主動去配合。在垃圾收集發生時,系統首先把所有用戶進程全部中斷,如果發現有用戶線程中斷的地方不在安全點上,就恢復這條線程執行,讓它一會再重新中斷,直到跑到安全點上
  • 主動式中斷
      當垃圾收集需要中斷線程的時候,不直接對線程操作,僅僅簡單地設置一個標誌位,各個線程執行過程時會不停的主動去輪詢這個標誌,一旦發現中斷標誌爲真的時候就自己在最近的安全點上主動中斷掛起,一旦發現中斷標誌爲真時就自己在最近的安全點上主動中斷掛起。

  輪詢標誌的地方和安全點是重合的,另外還要加上所有創建對象和其他需要在Java堆上分配內存的地方,這是爲了檢查是否即將要發生垃圾收集,避免沒有足夠內存分配新對象。

  HotSpot使用內存保護陷阱的方式,把輪詢操作精簡至只有一條彙編指令的程度。

安全區域

問題:
  安全點機制保證了程序執行時,在不太長的時間內就會可能遇到可進入垃圾的安全點但是程序不執行的時候(沒有分配處理器時間,例如用戶進程出於Sleep狀態或者是Blocked狀態),此時的線程無法響應虛擬機的中斷請求,不能在走到安全的地方去中斷掛起,虛擬機也不可能持續等待
安全區域:
  安全區域是指能夠確保在某一段代碼片段之中,引用關係不會發生變化,因此,在這個區域中任意地方開始垃圾收集都是安全的

記憶集

目的:
解決對象跨代引用帶來的問題
記憶集:
一種用於記錄從非收集區域指向收集區域的指針集合的抽象數據結構
僞代碼:

	//通過對象指針實現記憶集
Class RememberedSet {
Object[] set[OBJECT_INTERGENERATIONAL_REFERENCE_SIZE];
}
	//記憶集並不需要了解跨代指針的全部細節,只需要知道某塊非收集區域是否存在指向收集區域的指針即可

記憶集的性能指標

  • 字長精度
      每個記錄精確到一個機器字長(就是處理器的尋址位數),該字包含跨代指針
  • 對象精度
      每個記錄精確到一個對象,該對象中有字段含有跨代指針
  • 卡精度
      每個記錄精確到一塊內存區域,該區域內有對象韓有跨代指針

  卡表是記憶集的一種具體實現,定義了記憶集的記錄精度、與堆內存之間的映射關係等
  卡表與記憶集的關係可以類比成HashMap和Map的關係
  卡表最簡單的實現形式是字節數組(HotSpot虛擬機也是這樣做的)
   CARD_TABLE [this address >> 9] = 0;

字節數組

  CARD_TABLE中的每一個元素對應着表示的內存區域中一塊特定大小的內存塊(這個內存塊被稱作“卡頁”)。
  卡頁的大小是2的N次冪的字節數
  一個卡頁的內存中通常包含不止一個對象,只要卡頁中有一個(或者更多)對象的字段存在跨代指針,就將對應卡表的數組元素的值標示爲1,在GC發生的時候,根據卡表中的元素將包含跨代指針的卡頁加入GC Roots中

寫屏障

作用:
  維護卡表
寫屏障:
  類似於Spring裏面的Aop編程思想;寫屏障可以看做是在虛擬機層面對“引用類型字段賦值”這個動作的AOP切面,在引用對象賦值的時候回產生一個環形(Around)通知,供程序執行額外的動作,即賦值的前後都被寫屏障包含

經典的垃圾收集器

Seria收集器

特點:

  • 單線程收集器

  單線程不僅僅說明它只會使用一個處理器或一條收集線程去完成垃圾收集,而是說它在進行垃圾回收的時候,會把所有的其他的工作線程都停掉,直至它工作結束

  • 新生代收集器
    在這裏插入圖片描述
  • 簡單高效(與其他的收集器單線程相比)
  • 額外內存消耗最小(內存資源受限的環境)
  • 對於單核處理器或者處理器核心數較少的環境而言,Serial收集器由於沒有線程交互的開銷,專心做垃圾收集可以獲得最高的單線程收集效率

ParNew收集器

本質上是Serial收集器的多線程並行版本
能與CMS收集器配合工作
工作過程:
在這裏插入圖片描述

Parallel Scavenge收集器

  • 新生代收集器
  • 基於標記-複製算法實現
  • 目標是達到一個可控制的吞吐量(CMS收集器的關注點是儘可能地縮短垃圾收集時用戶線程的停頓時間)

吞吐量:處理器用於運行用戶代碼的時間與處理器總時間的壁紙
在這裏插入圖片描述

  • 提供了倆個擁有精確控制吞吐量的參數-XX

    1. MaxGCPauseMillis(控制最大垃圾收集停頓時間)

      MaxGCPauseMillis參數允許的值是一個大於0的毫秒數,收集器將盡力保證內存回收花費的時間不超過用戶設定值。如果把這個參數的值設置得更小一點並不能使得系統的垃圾收集速度變得更快。
      垃圾收集停頓時間縮短是以犧牲吞吐量和新生代空間爲代價換取的:系統把新生代調得小一些,收集300MB新生代肯定比收集500MB快,但這也直接導致垃圾收集發生得更頻繁,原來10秒收集一次、每次停頓100毫秒,現在變成5秒收集一次、每次停頓70毫秒。停頓時間的確在下降,但吞吐量也降下來了。

    1. GCTimeRatio(直接設置吞吐量的大小)

    GCTimeRatio參數的值則應當是一個大於0小於100的整數,也就是垃圾收集時間佔總時間的比率,相當於吞吐量的倒數。譬如把此參數設置爲19,那允許的最大垃圾收集時間就佔總時間的5%(即1/(1+19)),默認值爲99,即允許最大1%(即1/(1+99))的垃圾收集時間。

  • 垃圾收集的自適應的調節策略(GC Ergonomics)

  +UseAdaptiveSizePolicy,這是一個開關參數,當這個參數被激活之後,就不需要人工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代對象大小(-XX:PretenureSizeThreshold)等細節參數了,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時
間或者最大的吞吐量

Serial Old收集器

  • Serial收集器的老年代版本,使用標記-整理算法
  • 服務器模式下,倆種用途
    1. 在JDK 5以及之前的版本中與Parallel Scavenge收集器搭配使用
    2. 作爲CMS收集器發生失敗時的後備預案,在併發收集發生Concurrent Mode Failure時使用。

工作流程:
在這裏插入圖片描述

Parallel Old收集器

  •   Parallel Old是Parallel Scavenge收集器的老年代版本,支持多線程併發收集,基於標記-整理算法實現。
  •   新生代的Parallel Scavenge收集器一直處於相當尷尬的狀態,原因是如果新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old(PSMarkSweep)收集器以外別無選擇,其他表現良好的老年代收集器,如CMS無法與它配合工作。 由於老年代Serial Old收集器在服務端應用性能上的“拖累”,使用Parallel Scavenge收集器也未必能在整體上獲得吞吐量最大化的效果。
  •   Parallel Old收集器出現後,“吞吐量優先”收集器終於有了比較名副其實的搭配組合,在注重吞吐量或者處理器資源較爲稀缺的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器這個組合。
    工作流程:
    在這裏插入圖片描述

CMS收集器

   CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。目前很大一部分的Java應用集中在互聯網網站或者基於瀏覽器的B/S系統的服務端上,這類應用通常都會較爲關注服務的響應速度,希望系統停頓時間儘可能短,以給用戶帶來良好的交互體驗。

運作流程:

  1. 初始標記(CMS initial mark)
  2. 併發標記(CMS concurrent mark)
  3. 重新標記(CMS remark)
  4. 併發清除(CMS concurrent sweep)

  其中初始標記、重新標記這兩個步驟仍然需要“Stop The World”。初始標記僅僅只是標記一下GCRoots能直接關聯到的對象,速度很快;併發標記階段就是從GC Roots的直接關聯對象開始遍歷整個對象圖的過程,這個過程耗時較長但是不需要停頓用戶線程,可以與垃圾收集線程一起併發運行;而重新標記階段則是爲了修正併發標記期間,因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間通常會比初始標記階段稍長一些,但也遠比並發標記階段的時間短;最後是併發清除階段,清理刪除掉標記階段判斷的已經死亡的對象,由於不需要移動存活對象,所以這個階段也是可以與用戶線程同時併發的。

工作流程:
在這裏插入圖片描述
缺點:

  • 對處理器資源敏感

  CMS默認啓動的回收線程數是(處理器核心數量+3)/4
  如果處理器核心數在四個或以上,併發回收時垃圾收集線程只佔用不超過25%的處理器運算資源,並且會隨着處理器核心數量的增加而下降。但是當處理器核心數量不足四個時,CMS對用戶程序的影響就可能變得很大

  • CMS收集器無法處理“浮動垃圾”(Floating Garbage),有可能出現“Con-current Mode Failure”失敗進而導致另一次完全“Stop The World”的Full GC的產生。

  在CMS的併發標記和併發清理階段,用戶線程是還在繼續運行的,程序在運行自然就還會伴隨有新的垃圾對象不斷產生,但這一部分垃圾對象是出現在標記過程結束以後,CMS無法在當次收集中處理掉它們,只好留待下一次垃圾收集時再清理掉。這一部分垃圾就稱爲“浮動垃圾”。同樣也是由於在垃圾收集階段用戶線程還需要持續運行,那就還需要預留足夠內存空間提供給用戶線程使用,因此CMS收集器不能像其他收集器那樣等待到老年代幾乎完全被填滿了再進行收集,必須預留一部分空間供併發收集時的程序運作使用。在JDK 5的默認設置下,CMS收集器當老年代使用了68%的空間後就會被激活,這是一個偏保守的設置,如果在實際應用中老年代增長並不是太快,可以適當調高參數-XX:CMSInitiatingOccu-pancyFraction的值來提高CMS的觸發百分比,降低內存回收頻率,獲取更好的性能。到了JDK 6時,CMS收集器的啓動閾值就已經默認提升至92%。但這又會更容易面臨另一種風險:要是CMS運行期間預留的內存無法滿足程序分配新對象的需要,就會出現一次“併發失敗”(Concurrent Mode Failure),這時候虛擬機將不得不啓動後備預案:凍結用戶線程的執行,臨時啓用Serial Old收集器來重新進行老年代的垃圾收集,但這樣停頓時間就很長了。所以參數-XX:CMSInitiatingOccupancyFraction設置得太高將會很容易導致大量的併發失敗產生,性能反而降低,用戶應在生產環境中根據實際應用情況來權衡設置。

-CMS是一款基於“標記-清除”算法實現的收集器

  這意味着收集結束時會有大量空間碎片產生。空間碎片過多時,將會給大對象分配帶來很大麻煩,往往會出現老年代還有很多剩餘空間,但就是無法找到足夠大的連續空間來分配當前對象,而不得不提前觸發一次Full GC的情況。爲了解決這個問題,CMS收集器提供了一個-XX:+UseCMS-CompactAtFullCollection開關參數(默認是開啓的,此參數從JDK 9開始廢棄),用於在CMS收集器不得不進行Full GC時開啓內存碎片的合併整理過程,由於這個內存整理必須移動存活對象,(在Shenandoah和ZGC出現前)是無法併發的。這樣空間碎片問題是解決了,但停頓時間又會變長,因此虛擬機設計者們還提供了另外一個參數-XX:CMSFullGCsBefore-Compaction(此參數從JDK 9開始廢棄),這個參數的作用是要求CMS收集器在執行過若干次(數量由參數值決定)不整理空間的Full GC之後,下一次進入Full GC前會先進行碎片整理(默認值爲0,表示每次進入Full GC時都進行碎片整理)。

Garbage First收集器

Garbage First(簡稱G1)收集器是垃圾收集器技術發展歷史上的里程碑式的成果,它開創了收集器面向局部收集的設計思路和基於Region的內存佈局形式。G1是一款主要面向服務端應用的垃圾收集器。
目標: 替換CMS收集器

  JDK 9發佈之日,G1宣告取代Parallel Scavenge加Parallel Old組合,成爲服務端模式下的默認垃圾收集器,而CMS則淪落至被聲明爲不推薦使用(Deprecate)的收集器。

Mixed GC模式:
  G1可以面向堆內存任何部分來組成回收集(Collection Set,一般簡稱CSet)進行回收,衡量標準不再是它屬於哪個分代,而是哪塊內存中存放的垃圾數量最多,回收收益最大,這就是G1收集器的Mixed GC模式。

在G1收集器出現之前的所有其他收集器,包括CMS在內,垃圾收集的目標範圍要麼是整個新生代(Minor GC),要麼就是整個老年代(Major GC),再要麼就是整個Java堆(Full GC)。

  G1開創的基於Region的堆內存佈局是它能夠實現這個目標的關鍵。雖然G1也仍是遵循分代收集理論設計的,但其堆內存的佈局與其他收集器有非常明顯的差異:G1不再堅持固定大小以及固定數量的分代區域劃分,而是把連續的Java堆劃分爲多個大小相等的獨立區域(Region),每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。收集器能夠對扮演不同角色的Region採用不同的策略去處理,這樣無論是新創建的對象還是已經存活了一段時間、熬過多次收集的舊對象都能獲取很好的收集效果。
  Region中還有一類特殊的Humongous區域,專門用來存儲大對象。G1認爲只要大小超過了一個Region容量一半的對象即可判定爲大對象。每個Region的大小可以通過參數-XX:G1HeapRegionSize設定,取值範圍爲1MB~32MB,且應爲2的N次冪。而對於那些超過了整個Region容量的超級大對象,將會被存放在N個連續的Humongous Region之中,G1的大多數行爲都把Humongous Region作爲老年代的一部分來進行看待。
在這裏插入圖片描述
  G1收集器之所以能建立可預測的停頓時間模型,是因爲它將Region作爲單次回收的最小單元,即每次收集到的內存空間都是Region大小的整數倍,這樣可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。更具體的處理思路是讓G1收集器去跟蹤各個Region裏面的垃圾堆積的“價值”大小,價值即回收所獲得的空間大小以及回收所需時間的經驗值,然後在後臺維護一個優先級列表,每次根據用戶設定允許的收集停頓時間(使用參數-XX:MaxGCPauseMillis指定,默認值是200毫秒),優先處理回收價值收益最大的那些Region,這也就是“Garbage First”名字的由來。這種使用Region劃分內存空間,以及具有優先級的區域回收方式,保證了G1收集器在有限的時間內獲取儘可能高的收集效率。

低延遲垃圾收集器

衡量垃圾收集器的三項最重要的指標是:

  • 內存佔用(Footprint)
  • 吞吐量(Throughput)
  • 延遲(Latency)

Shenandoah收集器

  •   RedHat公司獨立發展,被Orcale(包括以前的Sun)公司排擠,只能用於OpenJDK
  •   目標是實現一種能在任何堆內存大小下都可以把垃圾收集的停頓時間限制在十毫秒以內的垃圾收集器,該目標意味着相比CMS和G1,Shenandoah不僅要進行併發的垃圾標記,還要併發地進行對象清理後的整理動作。
  • Shenandoah使用轉發指針讀屏障來實現併發整理
    相對於G1的改進:
  • 支持併發的整理算法,G1的回收階段是可以多線程並行的,卻不能與用戶線程併發。
  • Shenandoah不支持分代收集,沒有實現分代

  因爲出於性價比的權衡,基於工作量上的考慮將分代放到一個優先級比較低的位置上

  • 拋棄了G1中耗費大量內存和計算資源去維護的記憶集,改用名爲“連接矩陣”的全局數據結構來記錄跨Region的引用關係,降低了處理跨代指針時的記憶集維護消耗,也降低了僞共享問題的發生概率。

  連接矩陣可以簡單理解爲一張二維表格,如果Region N有對象指向Region M,就在表格的N行M列中打上一個標記
在這裏插入圖片描述

工作流程:

  • 初始標記
      首先標記GC Roots直接關聯的對象,該階段仍然是“Stop The World”的,但停頓時間與堆的大小無關,只與GC Roots的數量相關。
  • 併發標記
      遍歷全圖,標記出全部可達對象,該階段和用戶線程一起併發,時間長短取決於堆中存活對象的數量以及對象圖的結構複雜程度。
  • 最終標記
      處理剩餘的SATB掃描,並在這個階段統計出回收價值最高的Region,並將這些Region構成一組回收集(Collection Set)。最終標記階段也會有一小段短暫的停頓。
  • 併發清理
      該階段用於清理那些整個區域內連一個存活對象都沒有的Region(這類Region被成爲Immediate Garbage Region)
  • 併發回收
      該階段會把回收集裏面存活的對象先複製一份到其他未被使用的Region中。該階段的運行時長取決於回收集的大小。

  複製對象這件事情如果將用戶線程凍結起來再做是很簡單的,但如果是倆者必須要同時併發進行的話,就變得複雜了。困難點在於移動對象的同時,用戶的線程仍然可能不停對被移動的對象進行讀寫訪問,移動對象是一次性行爲,但移動後整個內存中,所有指向該對象的引用還是就對象的地址。這些很難一瞬間全部改變過來。
  Shenandoah通過讀屏障和被稱爲“Brooks Pointers”的轉發指針來解決這個問題

  • 初始引用更新
      併發回收階段複製對象結束以後,還需要把堆中所有引向舊對象的引用修正到複製後的新地址,這個操作稱爲引用更新。
      該階段時間很短,回產生一個非常短暫的停頓。

  引用更新的初始化階段實際上並未做什麼具體的處理,設立這個階段只是爲了建立一個線程集合點,確保所有併發回收階段中進行的收集器線程以完成分配給特曼的對象移動任務而已。

  • 併發引用更新
      開始進行真正的引用更新操作。這個階段是與用戶線程一起併發的,時間長短取決於內存中涉及的引用數量的多少。

  併發引用更新與併發標記不同,它不再需要沿着對象圖來搜索,只需要按照內存物理地址的順序,線性地搜索出引用類型,把舊值改爲新值即可。

  • 最終引用更新
      這個階段是Shenandoah的最後一次停頓,停頓時間只與GC Roots的數量相關。

  解決了堆中的引用更新後,還要修正存在於GC Roots中的引用。

  • 併發清理
      經過併發回收和引用更新之後,整個回收集中所有的Region已再無存活對象,這些Region都變成Immediate Garbage Regions了,最後再調用一次併發清理過程來回收這些Region的內存空間,供以後新對象分配使用。

  初始標記之前還有Initial Partial、Concurrent Partial和Final Partial階段,它們可以不太嚴謹地理解爲對應於以前分代收集中的Minor GC的工作;
  初試標記,併發標記,最終標記與G1一樣

ZGC收集器

  • 目標與Shenandoah高度相似,實現思路千差萬別。
  • ZGC的關鍵技術與PGC和C4只存在術語稱謂上的差別,實質幾乎一摸一樣。
    內存佈局:
  • 小型Region(Small Region):容量固定爲2MB,用於放置小於256KB的小對象。
  • 中型Region(Medium Region):容量固定爲32MB,用於放置大於等於256KB但小於4MB的對
    象。
  • 大型Region(Large Region):容量不固定,可以動態變化,但必須爲2MB的整數倍,用於放置4MB或以上的大對象。每個大型Region中只會存放一個大對象,這也預示着雖然名字叫作“大型Region”,但它的實際容量完全有可能小於中型Region,最小容量可低至4MB。大型Region在ZGC的實現中是不會被重分配的,因爲複製一個大對象的代價非常高昂。
    併發整理算法:
  • 讀屏障
  • 染色指針
      染色指針是一種直接將少量額外信息存儲在指針上的技術
      在64位系統中,理論可以訪問的內存高達16EB(2的64次冪)字節

  染色指針可以使得一旦某個Region的存活對象被移走之後,這個Region立即就能夠被釋放和重用掉,而不必等待整個堆中所有指向該Region的引用都被修正後才能清理。這點相比起Shenandoah是一個頗大的優勢,使得理論上只要還有一個空閒Region,ZGC就能完成收集,而Shenandoah需要等到引用更新階段結束以後才能釋放回收集中Region,這意味着堆中幾乎所有對象都存活的極端情況,需要1∶1複製對象到新Region的話,就必須要有一半的空閒Region來完成收集。
  染色指針可以大幅減少在垃圾收集過程中內存屏障的使用數量,設置內存屏障,尤其是寫屏障的目的通常是爲了記錄對象引用的變動情況,如果將這些信息直接維護在指針中,顯然就可以省去一些專門的記錄操作。實際上,到目前爲止ZGC都並未使用任何寫屏障,只使用了讀屏障(一部分是染色指針的功勞,一部分是ZGC現在還不支持分代收集,天然就沒有跨代引用的問題)。能夠省去一部分的內存屏障,顯然對程序運行效率是大有裨益的,所以ZGC對吞吐量的影響也相對較低。
  染色指針可以作爲一種可擴展的存儲結構用來記錄更多與對象標記、重定位過程相關的數據,以便日後進一步提高性能。現在Linux下的64位指針還有前18位並未使用,它們雖然不能用來尋址,卻可以通過其他手段用於信息記錄。如果開發了這18位,既可以騰出已用的4個標誌位,將ZGC可支持的最大堆內存從4TB拓展到64TB,也可以利用其餘位置再存儲更多的標誌,譬如存儲一些追蹤信息來讓垃圾收集器在移動對象時能將低頻次使用的對象移動到不常訪問的內存區域。

工作流程:

  • 併發標記
      與G1、Shenandoah相同的是,併發標記是遍歷對象圖做可達性分析的階段,前後也要經過類似於G1、Shenandoah的初始標記、最終標記(儘管ZGC中的名字不叫這些)的短暫停頓,而且這些停頓階段所做的事情在目標上也是相類似的。
      與G1、Shenandoah不同的是,ZGC的標記是在指針上而不是在對象上進行的,標記階段會更新染色指針中的Marked 0、Marked 1標誌位。
  • 併發預備重分配
      這個階段需要根據特定的查詢條件統計得出本次收集過程要清理哪些Region,將這些Region組成重分配集(Relocation Set)。

  ZGC劃分Region的目的並非爲了像G1那樣做收益優先的增
量回收。相反,ZGC每次回收都會掃描所有的Region,用範圍更大的掃描成本換取省去G1中記憶集的維護成本。因此,ZGC的重分配集只是決定了裏面的存活對象會被重新複製到其他的Region中,裏面的Region會被釋放,而並不能說回收行爲就只是針對這個集合裏面的Region進行,因爲標記過程是針對全堆的。此外,在JDK 12的ZGC中開始支持的類卸載以及弱引用的處理,也是在這個階段中完成的。

  • 併發重分配
      重分配是ZGC執行過程中的核心階段,這個過程要把重分
    配集中的存活對象複製到新的Region上,併爲重分配集中的每個Region維護一個轉發表(Forward Table),記錄從舊對象到新對象的轉向關係。得益於染色指針的支持,ZGC收集器能僅從引用上就明確得知一個對象是否處於重分配集之中,如果用戶線程此時併發訪問了位於重分配集中的對象,這次訪問將會被預置的內存屏障所截獲,然後立即根據Region上的轉發表記錄將訪問轉發到新複製的對象上,並同時修正更新該引用的值,使其直接指向新對象。

ZGC將這種行爲稱爲指針的“自愈”(Self-Healing)能力。這樣做的好處是隻有第一次訪問舊對象會陷入轉發,也就是隻慢一次,對比Shenandoah的Brooks轉發指針,那是每次對象訪問都必須付出的固定開銷,簡單地說就是每次都慢,因此ZGC對用戶程序的運行時負載要比Shenandoah來得更低一些。還有另外一個直接的好處是由於染色指針的存在,一旦重分配集中某個Region的存活對象都複製完畢後,這個Region就可以立即釋放用於新對象的分配(但是轉發表還得留着不能釋放掉),哪怕堆中還有很多指向這個對象的未更新指針也沒有關係,這些舊指針一旦被使用,它們都是可以自愈的。

  • 併發重映射
      重映射所做的就是修正整個堆中指向重分配集中舊對象的所
    有引用,這一點從目標角度看是與Shenandoah併發引用更新階段一樣的,但是ZGC的併發重映射並不是一個必須要“迫切”去完成的任務,因爲前面說過,即使是舊引用,它也是可以自愈的,最多隻是第一次使用時多一次轉發和修正操作。
      重映射清理這些舊引用的主要目的是爲了不變慢(還有清理結束後可以釋放轉發表這樣的附帶收益),所以說這並不是很“迫切”。因此,ZGC很巧妙地把併發重映射階段要做的工作,合併到了下一次垃圾收集循環中的併發標記階段裏去完成,反正它們都是要遍歷所有對象的,這樣合併就節省了一次遍歷對象圖的開銷。一旦所有指針都被修正之後,原來記錄新舊對象關係的轉發表就可以釋放掉了。

選擇合適的垃圾收集器

  • 應用程序應該關注什麼?
      如果是數據分析、科學計算類的任務,目標是能儘快算出結果,那吞吐量就是主要關注點;
      如果是SLA應用,那停頓時間直接影響服務質量,嚴重的甚至會導致事務超時,這樣延遲就是主要關注點;
      如果是客戶端應用或者嵌入式應用,那垃圾收集的內存佔用則是不可忽視的;
  • 運行應用的基礎設施如何?
      譬如硬件規格,要涉及的系統架構是x86-32/64、SPARC還是ARM/Aarch64;處理器的數量多少,分配內存的大小;選擇的操作系統是Linux、Solaris還是Windows等。
  • 使用JDK的發行商是什麼?版本號是多少?

內存分配與回收策略

對象優先在Eden分配

  大多數情況下,對象在新生代Eden區中分配。當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。

大多數對象直接進入老年代

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

動態對象年齡判定

  爲了能更好地適應不同程序的內存狀況,HotSpot虛擬機並不是永遠要求對象的年齡必須達到-XX:MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無須等到-XX:MaxTenuringThreshold中要求的年齡。

空間分配擔保

  在發生Minor GC之前,虛擬機必須先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那這一次Minor GC可以確保是安全的。如果不成立,則虛擬機會先查看-XX:HandlePromotionFailure參數的設置值是否允許擔保失敗(Handle Promotion Failure);如果允許,那會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將嘗試進行一次Minor GC,儘管這次Minor GC是有風險的;如果小於,或者-XX:
HandlePromotionFailure設置不允許冒險,那這時就要改爲進行一次Full GC。

垃圾收集器常用參數

在這裏插入圖片描述
在這裏插入圖片描述

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