JVM2:垃圾收集器与内存分配策略
垃圾收集器與內存分配策略
文章目錄
- 垃圾收集器與內存分配策略
- 對象回收
- 引用計數算法
- 可達性分析算法
- 四種引用類型
- 生存與死亡
- 回收方法區
- 垃圾收集算法
- 標記清除法
- 復制算法
- 標記-整理算法
- HotSpot的算法細節實現
- 根節點枚舉
- 安全點
- 安全區域
- 記憶集
- 寫屏障
- 并發的可達性分析
- 經典的垃圾收集器
- Serial收集器
- ParNew收集器
- Parallel Scavenge收集器
- Serial Old收集器
- Parallel Old收集器
- CMS收集器
- Garbage First收集器(G1收集器)
對象回收
引用計數算法
在對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加一;當引用失效時,計數器值就減一;任何時刻計數器為零的對象就是不可能再被使用的。
存在問題
單純的引用計數就很難解決對象之間相互循環引用的問題。
可達性分析算法
這個算法的基本思路就是通過一系列稱為“GC Roots”的根對象作為起始節點集,從這些節點開始,根據引用關系向下搜索,搜索過程所走過的路徑稱為“引用鏈”(Reference Chain),如果某個對象到GC Roots間沒有任何引用鏈相連,或者用圖論的話來說就是從GC Roots到這個對象不可達時,則證明此對象是不可能再被使用的。
可作為GC Roots的對象包括以下幾種:
四種引用類型
生存與死亡
即使在可達性分析算法中判定為不可達的對象,也不是“非死不可”的,這時候它們暫時還處于“緩刑”階段,**要真正宣告一個對象死亡,至少要經歷兩次標記過程:**如果對象在進行可達性分析后發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記,隨后進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。假如對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,那么虛擬機將這兩種情況都視為“沒有必要執行”。
**如果這個對象被判定為確有必要執行finalize()方法,那么該對象將會被放置在一個名為F-Queue的隊列之中,**并在稍后由一條由虛擬機自動建立的、低調度優先級的Finalizer線程去執行它們的finalize()方法。finalize()方法是對象逃脫死亡命運的最后一次機會,稍后收集器將對F-Queue中的對象進行第二次小規模的標記,==如果對象要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移出“即將回收”的集合;如果對象這時候還沒有逃脫,那基本上它就真的要被回收了。==從代碼清單中我們可以看到一個對象的finalize()被執行,但是它仍然可以存活。
/*** 此代碼演示了兩點:* 1.對象可以在被GC時自我拯救。* 2.這種自救的機會只有一次,因為一個對象的finalize()方法最多只會被系統自動調用一次* @author: Mr.O* @create: 2020-11-01 15:00**/ public class FinalizeEscapeGC {public static FinalizeEscapeGC SAVE_HOOK = null;public void isAlive() {System.out.println("yes, i am still alive :)");}@Overrideprotected 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 :(");}} }從代碼清單3-2的運行結果可以看到,SAVE_HOOK對象的finalize()方法確實被垃圾收集器觸發過,并且在被收集前成功逃脫了。
另外一個值得注意的地方就是,代碼中有兩段完全一樣的代碼片段,執行結果卻是一次逃脫成功,一次失敗了。這是因為任何一個對象的finalize()方法都只會被系統自動調用一次,如果對象面臨下一次回收,它的finalize()方法不會被再次執行,因此第二段代碼的自救行動失敗了。
回收方法區
方法區的垃圾收集主要回收兩部分內容:廢棄的常量和不再使用的類型。回收廢棄常量與回收Java堆中的對象非常類似。舉個常量池中字面量回收的例子,假如一個字符串“java”曾經進入常量池中,但是當前系統又沒有任何一個字符串對象的值是“java”,換句話說,已經沒有任何字符串對象引用常量池中的“java”常量,且虛擬機中也沒有其他地方引用這個字面量。如果在這時發生內存回收,而且垃圾收集器判斷確有必要的話,這個“java”常量就將會被系統清理出常量池。常量池中其他類(接口)、方法、字段的符號引用也與此類似。
判定一個常量是否“廢棄”還是相對簡單,而要判定一個類型是否屬于“不再被使用的類”的條件就比較苛刻了。需要同時滿足下面三個條件:
HotSpot虛擬機提供了-Xnoclassgc參數進行控制,還可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看類加載和卸載信息
垃圾收集算法
■新生代收集(Minor GC/Young GC):指目標只是新生代的垃圾收集。
■老年代收集(Major GC/Old GC):指目標只是老年代的垃圾收集。目前只有CMS收集器會有單獨收集老年代的行為。另外請注意“Major GC”這個說法現在有點混淆,在不同資料上常有不同所指,讀者需按上下文區分到底是指老年代的收集還是整堆收集。
■混合收集(Mixed GC):指目標是收集整個新生代以及部分老年代的垃圾收集。目前只有G1收集器會有這種行為。
標記清除法
算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成后,統一回收掉所有被標記的對象,也可以反過來,標記存活的對象,統一回收所有未被標記的對象。
它的主要缺點有兩個:
復制算法
它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。如果內存中多數對象都是存活的,這種算法將會產生大量的內存間復制的開銷,但對于多數對象都是可回收的情況,算法需要復制的就是占少數的存活對象,而且每次都是針對整個半區進行內存回收,分配內存時也就不用考慮有空間碎片的復雜情況,只要移動堆頂指針,按順序分配即可。這樣實現簡單,運行高效,不過其缺陷也顯而易見,這種復制回收算法的代價是將可用內存縮小為了原來的一半,空間浪費未免太多了一點。
HotSpot虛擬機默認Eden和Survivor的大小比例是8∶1
標記-整理算法
標記過程仍然與“標記-清除”算法一樣,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向內存空間一端移動,然后直接清理掉邊界以外的內存,“標記-整理”算法的示意圖如圖
如果移動存活對象,尤其是在老年代這種每次回收都有大量對象存活區域,移動存活對象并更新所有引用這些對象的地方將會是一種極為負重的操作,而且這種對象移動操作必須全程暫停用戶應用程序才能進行,這就更加讓使用者不得不小心翼翼地權衡其弊端了,像這樣的停頓被最初的虛擬機設計者形象地描述為“Stop The World”。
HotSpot的算法細節實現
根節點枚舉
我們以可達性分析算法中從GC Roots集合找引用鏈這個操作作為介紹虛擬機高效實現的第一個例子。固定可作為GC Roots的節點主要在全局性的引用(例如常量或類靜態屬性)與執行上下文(例如棧幀中的本地變量表)中,盡管目標明確,但查找過程要做到高效并非一件容易的事情,現在Java應用越做越龐大,光是方法區的大小就常有數百上千兆,里面的類、常量等更是恒河沙數,若要逐個檢查以這里為起源的引用肯定得消耗不少時間。
迄今為止,所有收集器在根節點枚舉這一步驟時都是必須暫停用戶線程的,因此毫無疑問根節點枚舉與之前提及的整理內存碎片一樣會面臨相似的“Stop The World”的困擾。
即使是號稱停頓時間可控,或者(幾乎)不會發生停頓的CMS、G1、ZGC等收集器,枚舉根節點時也是必須要停頓的。
**當用戶線程停頓下來之后,其實并不需要一個不漏地檢查完所有執行上下文和全局的引用位置,虛擬機應當是有辦法直接得到哪些地方存放著對象引用的。在HotSpot的解決方案里,是使用一組稱為OopMap的數據結構來達到這個目的。**一旦類加載動作完成的時候,HotSpot就會把對象內什么偏移量上是什么類型的數據計算出來,在即時編譯過程中,也會在特定的位置記錄下棧里和寄存器里哪些位置是引用。這樣收集器在掃描時就可以直接得知這些信息了,并不需要真正一個不漏地從方法區等GC Roots開始查找。
安全點
實際上HotSpot也的確沒有為每條指令都生成OopMap,前面已經提到,只是在“特定的位置”記錄了這些信息,這些位置被稱為安全點(Safepoint)。有了安全點的設定,也就決定了用戶程序執行時并非在代碼指令流的任意位置都能夠停頓下來開始垃圾收集,而是強制要求必須執行到達安全點后才能夠暫停。
安全點的選定既不能太少以至于讓收集器等待時間過長,也不能太過頻繁以至于過分增大運行時的內存負荷。
安全區域
安全區域是指能夠確保在某一段代碼片段之中,引用關系不會發生變化,因此,在這個區域中任意地方開始垃圾收集都是安全的。我們也可以把安全區域看作被擴展拉伸了的安全點。
記憶集
提到了為解決對象跨代引用所帶來的問題,垃圾收集器在新生代中建立了名為記憶集(Remembered Set)的數據結構,用以避免把整個老年代加進GC Roots掃描范圍。事實上并不只是新生代、老年代之間才有跨代引用的問題,所有涉及部分區域收集(Partial GC)行為的垃圾收集器,典型的如G1、ZGC和Shenandoah收集器,都會面臨相同的問題
寫屏障
我們已經解決了如何使用記憶集來縮減GC Roots掃描范圍的問題,但還沒有解決卡表元素如何維護的問題,例如它們何時變臟、誰來把它們變臟等。
卡表元素何時變臟的答案是很明確的——有其他分代區域中對象引用了本區域對象時,其對應的卡表元素就應該變臟,變臟時間點原則上應該發生在引用類型字段賦值的那一刻。
應用寫屏障后,虛擬機就會為所有賦值操作生成相應的指令,一旦收集器在寫屏障中增加了更新卡表操作,無論更新的是不是老年代對新生代對象的引用,每次只要對引用進行更新,就會產生額外的開銷,不過這個開銷與Minor GC時掃描整個老年代的代價相比還是低得多的。
在JDK 7之后,HotSpot虛擬機增加了一個新的參數-XX:+UseCondCardMark,用來決定是否開啟卡表更新的條件判斷。開啟會增加一次額外判斷的開銷,但能夠避免偽共享問題,兩者各有性能損耗,是否打開要根據應用實際運行情況來進行測試權衡。
并發的可達性分析
當前主流編程語言的垃圾收集器基本上都是依靠可達性分析算法來判定對象是否存活的,可達性分析算法理論上要求全過程都基于一個能保障一致性的快照中才能夠進行分析
可從GC Roots再繼續往下遍歷對象圖,這一步驟的停頓時間就必定會與Java堆容量直接成正比例關系了:堆越大,存儲的對象越多,對象圖結構越復雜,要標記更多對象而產生的停頓時間自然就更長
想解決或者降低用戶線程的停頓,就要先搞清楚為什么必須在一個能保障一致性的快照上才能進行對象圖的遍歷?為了能解釋清楚這個問題,我們引入三色標記
Wilson于1994年在理論上證明了,當且僅當以下兩個條件同時滿足時,會產生“對象消失”的問題,即原本應該是黑色的對象被誤標為白色:
因此,我們要解決并發掃描時的對象消失問題,只需破壞這兩個條件的任意一個即可。由此分別產生了兩種解決方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning,SATB)。
增量更新要破壞的是第一個條件,當黑色對象插入新的指向白色對象的引用關系時,就將這個新插入的引用記錄下來,等并發掃描結束之后,再將這些記錄過的引用關系中的黑色對象為根,重新掃描一次。這可以簡化理解為,黑色對象一旦新插入了指向白色對象的引用之后,它就變回灰色對象了。
原始快照要破壞的是第二個條件,當灰色對象要刪除指向白色對象的引用關系時,就將這個要刪除的引用記錄下來,在并發掃描結束之后,再將這些記錄過的引用關系中的灰色對象為根,重新掃描一次。這也可以簡化理解為,無論引用關系刪除與否,都會按照剛剛開始掃描那一刻的對象圖快照來進行搜索。
經典的垃圾收集器
這里垃圾收集器的算法筆記比較粗略,ZGC等收集器暫時不在這里做筆記了(由于水平有限,自己暫時也沒弄太明白),如果想要弄清楚,最好參考原書
如果兩個收集器之間存在連線,就說明它們可以搭配使用,圖中收集器所處的區域,則表示它是屬于新生代收集器抑或是老年代收集器。
Serial收集器
大家只看名字就能夠猜到,這個收集器是一個單線程工作的收集器,但它的“單線程”的意義并不僅僅是說明它只會使用一個處理器或一條收集線程去完成垃圾收集工作,更重要的是強調在它進行垃圾收集時,必須暫停其他所有工作線程,直到它收集結束。
它依然是HotSpot虛擬機運行在客戶端模式下的默認新生代收集器,有著優于其他收集器的地方,那就是簡單而高效(與其他收集器的單線程相比),對于內存資源受限的環境,它是所有收集器里額外內存消耗(Memory Footprint)[1]最小的;對于單核處理器或處理器核心數較少的環境來說,**Serial收集器由于沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率。**在用戶桌面的應用場景以及近年來流行的部分微服務應用中,分配給虛擬機管理的內存一般來說并不會特別大,收集幾十兆甚至一兩百兆的新生代(僅僅是指新生代使用的內存,桌面應用甚少超過這個容量),垃圾收集的停頓時間完全可以控制在十幾、幾十毫秒,最多一百多毫秒以內,只要不是頻繁發生收集,這點停頓時間對許多用戶來說是完全可以接受的。
ParNew收集器
ParNew收集器實質上是Serial收集器的多線程并行版本,除了同時使用多條線程進行垃圾收集之外,其余的行為包括Serial收集器可用的所有控制參數(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、對象分配規則、回收策略等都與Serial收集器完全一致
在JDK 5中使用CMS來收集老年代的時候,新生代只能選擇ParNew或者Serial收集器中的一個。ParNew收集器是激活CMS后(使用-XX:+UseConcMarkSweepGC選項)的默認新生代收集器,也可以使用-XX:+/-UseParNewGC選項來強制指定或者禁用它。
Parallel Scavenge收集器
Parallel Scavenge收集器也是一款新生代收集器,它同樣是基于標記-復制算法實現的收集器,也是能夠并行收集的多線程收集器……Parallel Scavenge的諸多特性從表面上看和ParNew非常相似
Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是盡可能地縮短垃圾收集時用戶線程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。
Parallel Scavenge收集器提供了兩個參數用于精確控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數以及直接設置吞吐量大小的-XX:GCTimeRatio參數。
Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器,使用標記-整理算法。
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多線程并發收集,基于標記-整理算法實現。
直到Parallel Old收集器出現后,“吞吐量優先”收集器終于有了比較名副其實的搭配組合,在注重吞吐量或者處理器資源較為稀缺的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器這個組合。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。這類應用通常都會較為關注服務的響應速度,希望系統停頓時間盡可能短,以給用戶帶來良好的交互體驗。CMS收集器就非常符合這類應用的需求。
從名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于標記-清除算法實現的,它的運作過程相對于前面幾種收集器來說要更復雜一些,整個過程分為四個步驟,包括:
1)初始標記(CMS initial mark)
2)并發標記(CMS concurrent mark)
3)重新標記(CMS remark)
4)并發清除(CMS concurrent sweep)
其中初始標記、重新標記這兩個步驟仍然需要“Stop The World”。
==CMS優點:==并發收集、低停頓,一些官方公開文檔里面也稱之為“并發低停頓收集器”(Concurrent Low Pause Collector)。
CMS缺點:
Garbage First收集器(G1收集器)
HotSpot開發團隊最初賦予它的期望是(在比較長期的)未來可以替換掉JDK 5中發布的CMS收集器。
作為CMS收集器的替代者和繼承人,設計者們希望做出一款能夠建立起“停頓時間模型”(PausePrediction Model)的收集器,停頓時間模型的意思是能夠支持指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間大概率不超過N毫秒這樣的目標(即停頓時間可預測)
在G1收集器出現之前的所有其他收集器,包括CMS在內,垃圾收集的目標范圍要么是整個新生代(Minor GC),要么就是整個老年代(Major GC),再要么就是整個Java堆(Full GC)。而G1跳出了這個樊籠,它可以面向堆內存任何部分來組成回收集(Collection Set,一般簡稱CSet)進行回收。這就是G1收集器的Mixed GC模式。
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作為老年代的一部分來進行看待。
用戶設定允許的收集停頓時間(使用參數-XX:MaxGCPauseMillis指定,默認值是200毫秒)
Region里面存在的跨Region引用對象如何解決?
使用記憶集避免全堆作為GC Roots掃描,但在G1收集器上記憶集的應用其實要復雜很多,它的每個Region都維護有自己的記憶集,這些記憶集會記錄下別的Region指向自己的指針,并標記這些指針分別在哪些卡頁的范圍之內。**G1的記憶集在存儲結構的本質上是一種哈希表,Key是別的Region的起始地址,Value是一個集合,里面存儲的元素是卡表的索引號。這種“雙向”的卡表結構(卡表是“我指向誰”,這種結構還記錄了“誰指向我”)比原來的卡表實現起來更復雜,同時由于Region數量比傳統收集器的分代數量明顯要多得多,因此G1收集器要比其他的傳統垃圾收集器有著更高的內存占用負擔。**根據經驗,G1至少要耗費大約相當于Java堆容量10%至20%的額外內存來維持收集器工作。
G1收集器的運作過程大致可劃分為以下四個步驟:
初始標記(Initial Marking):僅僅只是標記一下GC Roots能直接關聯到的對象,并且修改TAMS指針的值,讓下一階段用戶線程并發運行時,能正確地在可用的Region中分配新對象。這個階段需要停頓線程,但耗時很短,而且是借用進行Minor GC的時候同步完成的,所以G1收集器在這個階段實際并沒有額外的停頓。
并發標記(Concurrent Marking):從GC Root開始對堆中對象進行可達性分析,遞歸掃描整個堆里的對象圖,找出要回收的對象,這階段耗時較長,但可與用戶程序并發執行。當對象圖掃描完成以后,還要重新處理SATB記錄下的在并發時有引用變動的對象。
最終標記(Final Marking):對用戶線程做另一個短暫的暫停,用于處理并發階段結束后仍遺留下來的最后那少量的SATB記錄。
篩選回收(Live Data Counting and Evacuation):負責更新Region的統計數據,對各個Region的回收價值和成本進行排序,根據用戶所期望的停頓時間來制定回收計劃,可以自由選擇任意多個Region構成回收集,然后把決定回收的那一部分Region的存活對象復制到空的Region中,再清理掉整個舊Region的全部空間。這里的操作涉及存活對象的移動,是必須暫停用戶線程,由多條收集器線程并行完成的。
G1收集器除了并發標記外,其余階段也是要完全暫停用戶線程的,換言之,它并非純粹地追求低延遲,官方給它設定的目標是在延遲可控的情況下獲得盡可能高的吞吐量,所以才能擔當起“全功能收集器”的重任與期望。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ZHfEglHB-1604326359249)(/Users/duhaiyang/Library/Application Support/typora-user-images/截屏2020-11-02 下午10.02.50.png)]
相比CMS,G1的優點有很多,暫且不論可以指定最大停頓時間、分Region的內存布局、按收益動態確定回收集這些創新性設計帶來的紅利,單從最傳統的算法理論上看,G1也更有發展潛力。與CMS的“標記-清除”算法不同,G1從整體來看是基于“標記-整理”算法實現的收集器,但從局部(兩個Region之間)上看又是基于“標記-復制”算法實現,無論如何,這兩種算法都意味著G1運作期間不會產生內存空間碎片,垃圾收集完成之后能提供規整的可用內存。
總結
以上是生活随笔為你收集整理的JVM2:垃圾收集器与内存分配策略的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 技术人员如何创业:打造超强执行力团队
- 下一篇: transmit video