深刻理解java虛擬機之垃圾收集器 深刻理解java虛擬機之java內存區域 深刻理解java虛擬機之對象真的死了嗎 Java垃圾收集算法

 

  前言

  若是說收集算法是內存回收的方法論,那麼垃圾收集器就是內存回收的具體實現。java虛擬機規範中對垃圾收集器應該如何實現並無任何規定,所以不一樣的廠商、不一樣的版本的虛擬機所提供的垃圾收集器都有可能會有很大的區別,而且通常都會提供參數供用戶根據本身的應用特色和要求組合出各個年代所使用的收集器。html

  相關係列博客:java

  上圖中展現了不一樣年齡代的收集器,其中Serial、ParNew和Parallel Scavenge收集器做用於新生代,CMS、Parallel Old 和 Serial Old做用於老年代,G1在新生代和老年代均可以使用。不一樣的收集器之間若是有連線,則說明他們能夠相互搭配使用。算法

  相關概念

  並行:指的是多條垃圾收集線程一塊兒公共,可是此時用戶工做線程仍處於等待狀態。多線程

  併發:指的是用戶線程和垃圾收集線程同時工做,也有多是交替執行,用戶程序在繼續執行,而垃圾收集程序運行與另外一個CPU上。併發

  吞吐量:吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量 = 運行用戶代碼時間 /(運行用戶代碼時間 + 垃圾收集時間)。虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。post

 

  Serial收集器

  Serial收集器是一款串行執行的收集器,它是歷史最悠久,也是最基本的收集器,採用複製算法實現的新生代收集器。在jdk1.3之前,Serial收集器是新生代惟一的選擇。它是一個單線程執行的收集器,工做時只會知用一個cpu或線程區執行,更重要的是Serial在工做期間必須停掉全部的用戶線程,直至垃圾收集完成,這一過程咱們稱之爲「stop the world」。這項工做是由虛擬機自動執行和自動完成的,用戶在不知情的狀況下停掉了全部的線程,這對於一個最求響應速度來講簡直是沒法接受的。下圖展現了Serial收集器在工做時的運行流程:性能

  因爲Serial收集器的工做模式是單線程的,天然就沒有了多線程環境下線程切換帶來的性能開銷,因此該收集器在單線程環境下更加簡單高效。網站

 

  ParNew 收集器

  Parnew是Serial收集器的多線程版本,也是新生代收集器。ParNew收集器和Serial收集器除了多線程工做外幾乎是相同的,包括全部控制參數、收集算法、stop the world,對象分配規則,回收策略等都是同樣的。運行流程以下圖:url

  雖然與Serial收集器相比僅僅多了多線程特性外,沒有其它的創新之處,可是它倒是許多Server模式下的虛擬機新生代收集器的首選,緣由在於目前爲止只有Serial和ParNew兩個新生代收集器可以與性能優異的CMS配合使用。關於CMS介紹將在下文展開描述。線程

 

  Parallel Scavenge 收集器

  Parallel Scavenge也是一款使用複製算法的新生代收集器。該收集器與其它收集器不一樣的是,它關注的目標是達到一個可控制的吞吐量,而CMS等收集器的關注點則是儘量地減小用戶線程地停頓時間,提升用戶體驗。Parallel Scavenge收集器提供了兩個參數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數以及直接設置吞吐量大小的-XX:GCTimeRatio參數。也所以,Parallel Scavenge 被成爲「吞吐量優先」收集器。

  停頓時間越短就越適合與用戶交互較多地程序,這樣用戶體驗才更好。而高吞吐量則可讓出更多的cpu資源給用戶線程,讓程序更快的完成運算任務,更適合後臺運算較多而不須要與用戶交互的程序。

  自適應調節策略是Parallel Scavenge收集器的特色,也是與ParNew收集器的區別。Parallel Scavenge經過打開-XX:+UseAdaptiveSizePolicy的設置,就不須要手動地調節新生代(-Xmn)大小,Eden和Survivor區的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數,而是根據當前系統運行狀況來肯定這些參數,從而提升程序地吞吐量和縮短停頓時間,這一過程稱之爲GC自適應的調節策略(GC Ergonomics)。

  另外值得注意的一點是,Parallel Scavenge沒法已CMS配合使用,若是新生代選擇了Parallel Scavenge收集器,那麼老年代的收集器只能選用Serial Old或者Parallel Old來配置使用。

 

  Serial Old收集器

  Serial Old是Serial收集器的老年代版本,也是單線程工做的,使用的是「標記-整理」算法。

  該收集器主要用於Client模式下的虛擬機使用,若是在Server模式下能夠與Parallel Scavenge收集器配合使用;做爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。運行流程以下:

 

  Parallel Old收集器

  Parallel Old是Parallel Scavenge的老年代版本,也是一個並行收集器,使用「標記-整理」算法。該收集器在jdk1.6後對外提供使用,Parallel Scavenge 和  Parallel Old配合使用的話,更加適合應用於高吞吐量和cpu敏感資源的場合。下面是這兩個收集器配合使用的運行流程:

 

  CMS收集器

  CMS(Concurrent Mark Sweep)是一個併發收集器,使用了「標記-清除」算法來實現的。該收集器最求的更短的停頓時間,從而提高用戶體驗,所以也很是符合使用在網站、B/S系統的服務端的應用。

  CMS收集器的工做流程大概能夠分爲如下4個步驟:

  • 初始標記:這個階段僅僅標記可以和gc roots直接關聯的對象,速度很快,可是須要「stop the world」。
  • 併發標記:這個階段開始進行gc roots tracing標記,與用戶線程一塊兒執行的,消耗時間不少。
  • 從新標記:這個階段是要是修正在併發標記期間因爲用戶線程也在運行而產生標記變更的那部分對象的標記,比較耗費的時間比初始標記階段要長,可是遠比並發標記階段要短,這個過程也是須要「stop the world」的。
  • 併發清除:對無用對象進行回收操做。這個過程與用戶線程並行執行。

  因爲標記和清除階段能夠和用戶線程一塊兒工做,所以幾乎能夠把CMS收集器的工做是併發的:

  CMS是一款優秀的收集器,它的主要優勢是低停頓,併發收集,所以也被成爲併發低停頓收集器(Concurrent Low Pause Collector)。

  固然,CMS收集器也有必定的缺點,主要包括一下幾點:

  • CMS收集器使用「標記-清除」算法實現,所以不可避免地有內存碎片地問題。當內存碎片過多時,在分配大對象地過程當中即便有足夠的空間,可是找不到足夠地連續的空間來放該對象,那麼就有可能觸發一次full gc。
  • 沒法處理浮動垃圾(Floating Garbage) 可能出現「Concurrent Mode Failure」失敗而致使另外一次Full GC的產生。這是由於在標記的過程當中用戶線程也在運行着,那麼在這一過程當中出現的垃圾沒法當即回收,而是等下一次gc才能清理,我這部分的垃圾就叫作「浮動垃圾」。也是因爲在垃圾收集階段用戶線程還須要運行,那也就還須要預留有足夠的內存空間給用戶線程使用,所以CMS收集器不能像其餘收集器那樣等到老年代幾乎徹底被填滿了再進行收集,須要預留一部分空間提供併發收集時的程序運做使用。
  • 對cpu資源很是敏感。其實,只要是面對併發的狀況下都會有這個問題,在併發階段雖然不會中斷用戶線程,可是由於佔用了部分用戶的資源而致使程序變慢,總吞吐量下降。CMS蒐集器默認的線程數 = (cpu核數 + 3) / 4,當cpu數量大於4時,垃圾回收線程數很多於25%,隨着線程數的增長而降低,當cpu數量小於4時對線程的執行效率有顯著的影響。

  運行示意圖以下:

  

  G1收集器

  G1(Garbage-First)是一款面向服務端應用的垃圾收集器,JDK 7 Update4 後開始進入商用。HotSpot開發團隊賦予它的使命是將來能夠替換掉JDK 1.5中發佈的CMS收集器。以前提供的收集器都是僅做用於新生代或者是老年代,可是G1收集器能夠做用於新生代和老年代,由於使用G1收集器是java heap的內存結構有很大的不一樣,它將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,可是他們已經沒有了物理上的隔閡了,它們都是region的一部分的集合。

  G1(Garbage-First)收集器是當今收集器技術發展的最前沿成果之一,與其餘收集器相比,G1收集器具備如下特徵:

  • 並行與併發: G1能充分利用多CPU,多核環境下的硬件優點,使用多個CPU來縮短Stop-The-World停頓時間,部分其餘收集器本來須要停頓java線程執行的GC動做,G1收集器仍然能夠經過併發的方式讓java程序繼續執行。
  • 分代收集: 與其餘收集器同樣,分代概念在G1中仍然得以保留。雖然G1能夠不須要其餘收集器配合可以獨立管理整個堆,但它可以採用不用的方式去處理新創的對象和已經存活了一段世紀那、熬過屢次GC的舊對象以得到更好的收集效果。
  • 空間整合: 與CMS的「標記-清除」算法不一樣,G1總體來看採用了「標記-整理」算法實現的收集器,從局部(兩個Region之間)上來看是基於「複製」算法實現的。不管使用哪種方法,都意味着G1運做期間不會產生內存空間碎片的問題,收集後能提供規整的可用空間。這種特性有利於程序長時間運行,分配大對象是不會由於沒法獲得連續內存空間而提早處罰一次GC。
  • 可預測的停頓: 這是G1相對於CMS的另外一大優點,下降停頓時間是G1和CMS共同的關注點,但G1除了最求低停頓外,還能創建可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片斷內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎是java(RTSJ)的垃圾收集器的特徵了。

  G1收集器之因此可以創建可預測的停頓時間模型,由於他可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收所得到的空間大小以及回收所須要的經驗值),在後臺維護一個優先表,每次根據容許的收集時間,優先回收價值最大的Region。這種使用Region劃份內存空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間內能夠獲取儘量高的收集效率。

  在G1收集器中,Region之間的對象引用以及其餘收集器中的新生代和老年代之間的對象引用,虛擬機都是使用Remembered Set來避免全堆掃描的。G1中每一個Region都有一個與之對應的Remembered Set,虛擬機發現程序在對Reference類型的數據進行寫操做時,會產生一個Write Barrier暫時中斷寫操做,檢查Reference引用的對象是否處於不一樣的Region中,若是是,便經過CardTable把相關引用信息記錄到被引用對象所屬的Region中的Remembered Set之中。當進行內存回收時,在GC根節點的枚舉範圍中加入Remembered Set便可保證不對全堆掃描,也不會有遺漏。

  若是不計算維護Remembered Set的操做,G1收集器的運做大體分爲如下幾個步驟:

  1. 初始標記(Initial Marking): 這階段僅僅只是標記GC Roots能直接關聯到的對象並修改TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序併發運行時,能在正確的可用的Region中建立新對象,這階段須要停頓線程,可是耗時很短。
  2. 併發標記(Concurrent Marking): 從GC Roots 開始對堆的對象進行可達性分析,找出存活的對象,這階段耗時長,可是能夠與用戶程序併發執行。
  3. 最終標記(Final Marking): 爲了修正在併發標記期間由於用戶程序繼續運行而致使標記產生變更的那一部分標記記錄,虛擬機將這段時間對象變化記錄記錄在線程Remembered Set Logs裏面。
  4. 篩選回收(Live Data Counting and Evacuation): 首先對各個Region的回收價值和成本進行排序,根據用戶所指望的GC停頓時間來制定回收計劃,這一階段是能夠與用戶程序一塊兒併發執行的,可是由於只回收部分Region,時間是用戶可控的,並且停頓用戶線程將大幅度提升收集效率。

  執行流程以下圖:

  

  總結

  

收集器 串行、並行or併發 新生代/老年代 算法 目標 適用場景
Serial 串行 新生代 複製算法 響應速度優先 單CPU環境下的Client模式
Serial Old 串行 老年代 標記-整理 響應速度優先 單CPU環境下的Client模式、CMS的後備預案
ParNew 並行 新生代 複製算法 響應速度優先 多CPU環境時在Server模式下與CMS配合
Parallel Scavenge 並行 新生代 複製算法 吞吐量優先 在後臺運算而不須要太多交互的任務
Parallel Old 並行 老年代 標記-整理 吞吐量優先 在後臺運算而不須要太多交互的任務
CMS 併發 老年代 標記-清除 響應速度優先 集中在互聯網站或B/S系統服務端上的Java應用
G1 併發 both 標記-整理+複製算法 響應速度優先 面向服務端應用,未來替換CMS

 

  參考資料: 《深刻理解Java虛擬機-JVM高級特性與最佳實踐》 -周志明

  喜歡我寫的博客的同窗能夠關注訂閱號【Java解憂雜貨鋪】,裏面不按期發佈一些技術幹活,也能夠免費獲取大量最新最流行的技術教學視頻

相關文章
相關標籤/搜索