前面有篇文章介紹了JVM的基本信息,而衆所周知,Java帶有垃圾收集機制,那本篇就介紹一下Java中的垃圾收集算法與垃圾收集器。html
朋友,再花20分鐘瞭解下噻 (嘿嘿...)...算法
在Java開發中,開發者無需關注動態分配與垃圾回收,更加專一於開發事項,那麼本篇就介紹一下關於GC (Garbage Collection)方面的一些內容:安全
此處提問一個問題:Java中程序計數器,本地方法棧等是線程私有的區域,隨着線程的消失而消除,那麼此處的垃圾收集是如何進行的呢?服務器
如何斷定一個Java對象(堆中對象)已經成爲垃圾對象,或對象已經死亡呢?有兩種方式:數據結構
1.引用計數算法:經過判斷對象的引用數量,決定對象是否能夠被回收。多線程
給對象中添加一個引用計數器,每有一個地方引用它時,計數器值加1;當引用失效時,計數器值減1;任什麼時候刻計數器值爲0表示對象不能再被使用。併發
這種算法的實現簡單,效率高,但很難解決對象之間循環引用的問題。(Java中不採用此算法)性能
2.可達性分析算法:經過判斷對象的引用鏈是否與根節點可達,決定對象是否能夠被回收。測試
根搜索算法是經過一些「GC Roots」對象做爲起點,從這些節點開始往下搜索,搜索經過的路徑成爲引用鏈(Reference Chain),當一個對象沒有被GC Roots 的引用鏈鏈接的時候,說明這個對象是不可用的。優化
擴展:若是某一對象與GCRoots沒有引用,則必定會進行回收嗎?能夠看一下對象的自我救贖。
那判斷完對象是垃圾對象,按照什麼樣的方式(算法)對垃圾對象/死亡對象進行回收呢?
1.標記-清楚算法(Mark-Sweep)
該算法分爲標記、清除兩個階段,先標記處全部須要回收的對象,在標記完成後統一回收全部被標記的對象。是最基礎的收集算法,後續的收集算法都是基於這種思路並對其不足進行改進而獲得的。
它存在兩個不足:一是效率問題,由於標記和清楚的過程效率都不高,二是空間問題,標記清除後會產生大量不連續的內存碎片,空間碎片太多可能會致使之後在程序運行過程當中須要分配較大對象時,沒法找到足夠的內存而不得不提早觸發另外一次垃圾收集動做。
2.複製算法(Copy)
複製算法將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。這種算法適用於對象存活率低的場景,好比新生代。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。
複製收集算法在對象存活率較高時就要進行較多的複製操做,效率將會變低。
3.標記-整理算法(Mark-Compact)
標記—整理算法和標記—清除算法同樣,可是標記—整理算法不是把存活對象複製到另外一塊內存,而是把存活對象往內存的一端移動,而後直接回收邊界之外的內存。標記—整理算法提升了內存的利用率,而且它適合在收集對象存活時間較長的老年代。
注:標記整理算法與標記清除算法最顯著的區別是:標記清除算法不進行對象的移動,而且僅對不存活的對象進行處理;而標記整理算法會將全部的存活對象移動到一端,並對不存活對象進行處理,所以其不會產生內存碎片。
分代收集算法
當前商用的虛擬機都採用的是 「分代收集」 算法,其實質上是前三個算法理論在JVM中的一個區域分佈,至關於前面三個的綜合。
不一樣的對象的生命週期(存活狀況)是不同的,而不一樣生命週期的對象位於堆中不一樣的區域,所以對堆內存不一樣區域採用不一樣的策略進行回收能夠提升 JVM 的執行效率。通常把Java堆分爲新生代和老年代。新生代中,每次垃圾收集都發現有大批對象死去,只有少許存活,就用複製算法;老年代中,對象存活率高、沒有額外空間對它進行分配擔保,就用「標記-清除」或「標記-整理」算法。
在JVM內存區域章節中,介紹了JVM的劃分,而堆空間中分爲了新生代、老年代等空間,每一個空間的特性決定其使用不一樣的GC算法。
確認了垃圾對象,且瞭解了按照什麼樣的邏輯進行垃圾對象收集,那具體的是由什麼來進行收集操做的呢?
在瞭解這個內容以前,先得有一個小的討論,討論幾下幾點:
1.既然每一個算法中有有標記相關的一個過程,那麼在標記時,整個程序是並行的呢?仍是直接中止程序,再進行呢?
按照圖上理論階段的標記來說,若是程序再執行中進行垃圾對象標記,則其中會發生大機率的遺漏標記的發生,還有一個是若是某一GCRoot是垃圾對象呢?所以保證該階段的一致性,此階段是中止程序的。有一個淺顯的例子:你媽打掃房間時,你在扔垃圾,那這種狀況下,垃圾會確定會有遺漏的吶,估計孩子可能被打S吧...因此按照這個邏輯來說,此時在標記執行的時間點或時間段,程序猶如凍結通常,這個被稱爲STW事件(Stop The World),中止世界,意思是GC標記時須要停頓全部的Java執行線程。
STW緣由:由於可達性分析算法必須是在一個確保一致性的內存快照中進行。若是在分析的過程當中對象引用關係還在不斷變化,分析結果的準確性就不能保證。
2.接上個問題,若是標記階段程序停頓,那麼何時,什麼時間點停頓呢?如何選擇該時間呢?
STW事件發生的是何時?這個就得從與GCRoots的引用鏈開始介紹了。
首先得清除哪些對象是GCRoots,這個有兩種方式:一是遍歷方法區和棧區查找(保守式 GC,消耗過高)。二是經過 OopMap 數據結構來記錄 GC Roots 的位置(準確式 GC,且OopMap能夠做爲安全點,記錄該位置可停頓)。
安全點的做用主要使程序再特定的位置能夠停頓,進行GC標記。安全點意味着在這個點時,全部工做線程的狀態是肯定的,JVM 就能夠安全地執行 GC 。
那麼停頓也有兩種:一是搶先式中斷,發生GC時,若程序沒有停頓到安全點,則恢復並達到安全點時停頓,二是主動式中斷,發生GC時,不直接操做線程中斷,而是簡單地設置一個標誌,讓各個線程執行時主動輪詢這個標誌,發現中斷標誌爲真時就本身中斷掛起。JVM 採起的就是主動式中斷。輪詢標誌的地方和安全點是重合的。
安全點(safe Point)主要是針對於正在執行的線程來決定的,那麼對於正在阻塞或休眠的線程呢?這個如何處理呢?
--- 若是線程處於中斷狀態,則未必會恰好停頓到安全點上,此時引入了一個安全區(Safe Region)的概念,這個表示在該區域內,GC的引用關係是不會發生變化的,任意時刻進行回收都是安全的。
這兒藉助一下參考博客:http://www.javashuo.com/article/p-zfzzphhv-dk.html
http://wuzhangyang.com/2019/01/18/understand-safe-point/
3.同理,標記對象以後的清除階段是並行進行的呢?仍是中止程序,再進行清除操做呢?這個清除的過程程序會如何進行呢?有什麼具體的操做演示呢?
同理,按照現有的理論來說,若對象已經被標記爲垃圾對象了,那麼此時直接進行清除處理便可,這個無關程序是否停頓,隨意啦...
並且清除是不是串行仍是並行,這個是與垃圾收集器相關的。
串行採用單線程處理,適用於單CPU或併發能力弱的系統,當回收器啓動後會暫停工做線程,更因爲是單線程,致使其STW的時間會很長
並行採用多線程處理,適用於多CPU或併發能力強的系統,當回收器啓動後也會暫停其餘工做線程,但因爲是多線程,會減小其STW時間
------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------ ------
垃圾收集器 是隨着JDK版本的升級而不斷的優化,這與JDK不斷髮展是息息相關的,所以下面介紹的就是JDK中目前已存在的垃圾收集器。
1.Serial 收集器
最早的收集器,單線程,採用複製算法,用於新生代,進行垃圾回收時,會產生STW,主要適用於單核CPU的場景,能夠避免多線程上下文切換帶來的消耗。單線程:只會使用一條垃圾收集線程去完成垃圾收集工做,更重要的是它在進行垃圾收集工做的時候必須暫停其餘全部的工做線程( 「Stop The World」 ),直到它收集結束。
2.ParNew 收集器
Serial收集器的多線程版本,除了使用多線程進行垃圾收集外,其他行爲(控制參數、收集算法、回收策略等等)和Serial收集器徹底同樣,也是採用複製算法,一樣會產生STW。
3.parallel Scavenge 收集器
Parallel Scavenge 收集器相似於ParNew 收集器。Parallel Scavenge收集器關注點是吞吐量(高效率的利用CPU)。因爲與吞吐量關係密切,Parallel Scavenge收集器也常常稱爲「吞吐量優先」收集器。
吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間 + 垃圾收集時間)。提供了兩個參數來控制吞吐量。
-XX:MaxGCPauseMillis 控制最大垃圾收集停頓時間,大於0的毫秒數
-XX:GCTimeRatio 設置吞吐量大小,大於0且小於100的整數,即垃圾收集時間佔總時間的比率
4.Serial Old 收集器
Serial收集器的老年代版本,它一樣是一個單線程收集器,採用標記-整理算法。它主要有兩大用途:一種用途是在JDK1.5以及之前的版本中與Parallel Scavenge收集器搭配使用,另外一種用途是做爲CMS收集器的後備方案。
5.Parallel Old 收集器
Parallel Scavenge收集器的老年代版本。使用多線程和「標記-整理」算法。在注重吞吐量以及CPU資源的場合,均可以優先考慮 Parallel Scavenge收集器和Parallel Old收集器。
6.CMS 收集器
CMS(Concurrent Mark Sweep)垃圾收集器的關注點更多的是用戶線程的停頓時間(提升用戶體驗),是一種以獲取最短回收停頓時間爲目標的收集器。它而很是符合在注重用戶體驗的應用上使用。CMS(Concurrent Mark Sweep)收集器是HotSpot虛擬機第一款真正意義上的併發收集器,它第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工做。從名字中的Mark Sweep這兩個詞能夠看出,CMS收集器是一種 「標記-清除」算法實現的,它的運做過程相比於前面幾種垃圾收集器來講更加複雜一些。
CMS主要優勢:併發收集、低停頓。可是它有下面三個明顯的缺點:對CPU資源敏感;沒法處理浮動垃圾;它使用的回收算法-「標記-清除」算法會致使收集結束時會有大量空間碎片產生。
7.G1 收集器
G1 (Garbage-First)是一款面向服務器的垃圾收集器,主要針對配備多顆處理器及大容量內存的機器, 以極高機率知足GC停頓時間要求的同時,還具有高吞吐量性能特徵。
G1將Java堆內存劃分爲不少個大小相等的獨立區域Region,對新生代老年代也沒有物理上的劃分了,都是多個Region的集合。所以G1是做用在新生代和老年代的。從總體上看是使用的標記-整理算法,也避免了空間碎片的問題,從Region局部來看也有複製算法。
G1收集器在後臺維護了一個優先列表,每次根據容許的收集時間,優先選擇回收價值最大的Region(這也就是它的名字Garbage-First的由來)。這種使用Region劃份內存空間以及有優先級的區域回收方式,保證了GF收集器在有限時間內能夠儘量高的收集效率(把內存化整爲零)。
8.ZGC 收集器 (目前只支持Linux)
Java 11包含一個全新的垃圾收集器--ZGC,設計目標是:支持TB級內存容量,暫停時間低(<10ms),對整個程序吞吐量的影響小於15%。 未來還能夠擴展實現機制,以支持很多使人興奮的功能,例如多層堆(即熱對象置於DRAM和冷對象置於NVMe閃存),或壓縮堆。
ZGC給Hotspot Garbage Collectors增長了兩種新技術:着色指針和讀屏障。
着色指針(colored pointers):一種將信息存儲在指針(或使用Java術語引用)中的技術。由於在64位平臺上(ZGC僅支持64位平臺),指針能夠處理更多的內存,可以使用一些位來存儲狀態。而取消着色時,採用了多重映射的技巧(沒明白);
讀屏障:每當應用程序線程從堆加載引用時運行的代碼片斷(即訪問對象上的非原生字段non-primitive field),讀屏障的工做是經過測試加載的引用來執行此任務,以查看是否設置了某些位。
參考文檔:https://blog.csdn.net/j3t9z7h/article/details/87128403
https://www.cnblogs.com/huanchupkblog/p/10947919.html
https://www.wangxinshuo.cn/2018/09/09/G1/ZGC/
9.Shenendoah 收集器
JDK12中的新版本垃圾收集器--Shenandoah收集器,一款concurrent及parallel的垃圾收集器;跟ZGC同樣也是面向low-pause-time的垃圾收集器,不過ZGC是基於colored pointers來實現,而Shenandoah GC是基於brooks pointers來實現。
參看文檔:https://blog.csdn.net/qq_33330687/article/details/90314347
停頓時間:垃圾收集器作垃圾回收中斷應用執行的時間。-XX:MaxGCPauseMillis
吞吐量:垃圾收集的時間和總時間的佔比:1/(1+n),吞吐量爲1-1/(1+n)。-XX:GCTimeRatio=n
(願你的每一行代碼,都有讓世界進步的力量 ------ fn)