你必須瞭解的java內存管理機制(四)-垃圾回收

本文在我的技術博客不一樣步發佈,詳情可用力戳
亦可掃描屏幕右側二維碼關注我的公衆號,公衆號內有我的聯繫方式,等你來撩...html

相關連接(注:文章講解JVM以Hotspot虛擬機爲例,jdk版本爲1.8)
一、 你必須瞭解的java內存管理機制-運行時數據區
二、 你必須瞭解的java內存管理機制-內存分配
三、 你必須瞭解的java內存管理機制-垃圾標記
四、 你必須瞭解的java內存管理機制-垃圾回收java

前言

  在前面三篇文章中,對JVM的內存佈局、內存分配、垃圾標記作了較多的介紹,垃圾都已經標記出來了,那剩下的就是如何高效的去回收啦!這篇文章將重點介紹如何回收舊手機、電腦、彩電、冰箱~啊呸(⊙o⊙)…將重點介紹幾種垃圾回收算法、HotSpot中經常使用的垃圾收集器的主要特色和應用場景。同時,這篇文章也是這個系列中的最後一篇文章啦!算法

正文

  上一篇文章中,咱們詳細介紹了兩種標記算法,而且對可達性分析算法作了較多的介紹。咱們也知道了HotSpot在具體實現中怎麼利用OopMap+RememberedSet的技術作到「準確式GC」。無論使用什麼優化的技術,目標都是準確高效的標記回收對象!那麼,爲了高效的回收垃圾,虛擬機又經歷了哪些技術及算法的演變和優化呢?(注:G1收集器及回收算法本文不涉及,由於我以爲後面能夠單獨寫一篇文章來談!)多線程

回收算法

  在這裏,咱們會先介紹幾種經常使用的回收算法,而後瞭解在JVM中式如何對這幾種算法進行選擇和優化的。併發

標記-清除

  "標記-清除"算法分爲兩個階段,「標記」和「清除」。標記仍是那個標記,在上一篇文章中已經作了較多的介紹了,JVM在執行完標記動做後,還在"即將回收"集合的對象將被統一回收。執行過程以下圖:框架

  

  優勢:
    一、基於最基礎的可達性分析算法,它是最基礎的收集算法。
    二、後續的收集算法都是基於這種思路並對其不足進行改進而獲得的。
  缺點:
    一、 執行效率不高。
    二、 由上圖能看到這種回收算法會產生大量不連續內存碎片,若是這時候須要建立一個大對象,則沒法進行分配。佈局

複製算法

  「複製」算法將內存按容量劃分爲大小相等的兩塊,每次使用其中的一塊。當一塊的內存用完了,就將還存活的對象複製到另外一塊上面,而後將已經使用過的存儲空間一次性清理掉,這樣每次都是針對整個半區的內存進行回收,不用考慮碎片問題。執行過程以下圖:優化

  

  優勢:
    一、每次針對半個區域進行回收,實現簡單,運行高效。
    二、不會產生內存碎片問題。
  缺點:
    一、 內存會縮小爲原來的通常,代價高。
    二、 當對象存活率較高時,須要進行較多複製操做,效率將會變低。線程

複製算法改良版

  「複製算法改良版」替代原來將內存一分爲二的方案,將內存分爲一塊較大的內存(稱爲Eden空間)和兩塊較小的內存(稱爲Survivor空間),每次使用Eden空間和其中一塊Survivor空間。當回收時,將Eden和其中一塊Survivor中還存活的對象一次性複製到另一塊Survivor空間上,最後清理掉Eden和剛纔使用過的Survivor空間。執行過程以下圖:3d

  

  優勢:
    一、改善了普通複製算法的缺點,提升了空間利用率

標記-整理算法

  「標記-整理」算法的標記過程與「標記-清除」算法是同樣同樣的,但後續步驟不是直接對可回收對象進行清理,而是讓全部的對象都向一端移動,而後直接清理掉端邊界之外的內存。執行過程以下圖:

  

  優勢:
    一、改善了「標記-清除」算法會產生內存碎片的缺點。
    二、不會像「複製」算法那樣效率隨對象存活率升高而變低。
  缺點:
    一、 依然沒有解決 「標記-清除」算法存在的缺點,那就是回收效率問題。還多了須要整理的過程,效率更低。

分代收集算法

  咱們都知道,在主流的虛擬機中都是採用分代收集算法來進行堆內存的回收,在第一篇文章中咱們也用了一張圖展現了JVM堆內存的劃分。以下:

  

  分代回收根據對象存活週期的不一樣將內存劃分爲幾塊,這樣就能夠根據各個年代的特色採用最適當的收集算法。通常把Java堆分爲新生代老年代

  新生代

  在Hotspot虛擬機中,新生代的收集器都是採用的改良版的複製算法進行垃圾回收。將新生代一分爲三,一塊Eden區和兩塊Survivor區。Eden區與兩塊Survivor區的比例爲8:1:1。這樣劃分的依據是什麼呢?基於弱代理論,IBM研究代表新生代中98%的對象都是"朝生夕死",大多數分配了內存的對象並不會存活太長時間,在處於年輕代時就會死掉。

  在原始的複製算法中,空間一分爲二,空間利用率爲50%,也就是說有新生代中50%的空間會被浪費,沒法分配內存。Hotspot虛擬機使用改良的複製算法,而且設置合理的空間比例,新生代中可用的內存空間爲整個新生代容量的90%,只有10%的空間會被浪費,大大的提升的新生代的空間利用率。若是存活對象佔用的內存大於新生代容量的10%怎麼辦?這就須要依賴其餘內存(老年代)進行分配擔保了。新生代回收動圖以下:

  

  老年代

  因爲老年代的對象存活週期通常相對較長,不會像新生代對象那樣「朝生夕死」,因此對象存活率高是老年代的特色,而且老年代也沒有額外的空間能夠分配擔保,因此不適合採用複製算法進行回收。根據老年代的特色,通常會使用"標記-清理"或"標記-整理"算法來進行垃圾回收。

收集器

  上面咱們介紹了在JVM中經常使用的垃圾回收算法及每一種算法的優缺點。接下里會介紹在HotSpot虛擬機中經常使用的幾種垃圾收集器,垃圾收集器是垃圾回收算法的具體實現,不一樣的商家、不一樣版本的JVM所提供的垃圾收集器可能會存在差別。這幾種收集器分別是Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1。在瞭解垃圾收集器以前,咱們先來區分幾個概念:

  併發收集器VS並行收集器
  並行:指多條收集線程同時進行收集工做,但此時用戶線程處於等待狀態。如ParNew、Parallel Scavenge、Parallel Old。
  併發:指用戶線程與垃圾收集線程同時執行(並不必定是並行,可能會交替執行)。如CMS、G1。

  YoungGC VS OldGC VS MinorGC VS MajorGC VS FullGC
  Minor GC、YoungGC:Minor GC又稱爲新生代GC,因此等價於Young GC,在新生代的Eden區分配滿的時候觸發。在Young GC後新生代中有部分存活對象會晉升到老年代,有多是年齡達到閾值(默認爲15歲,在JVM裏面15歲就步入老年生活了,O(∩_∩)O哈哈~)了,也多是Survivor區域滿了,若是是Survivor區域被填滿,會將全部新生代中存活的對象移動到老年代中!

  Major GC、Old GC、Full GC:Old GC從字面能理解是老年代的GC,可是對Major GC和Full GC存在多種說法,有的認爲Major GC等價於Old GC只是針對老年代的GC,有的認爲Major GC和Full GC是等價的。可是我我的認爲Major是指老年代GC,而Full GC針對新生代、老年代、永久代整個的回收。因爲老年代的GC都會伴隨一次新生代的GC,因此習慣性的把Major GC和Full GC劃上了等號。前面Young GC時候說到「在Young GC後新生代中有部分存活對象會晉升到老年代」,萬一老年代的空間不夠存放新生代晉升的對象怎麼辦呢?因此當準備要觸發一次Young GC時,若是發現統計數據以前Young GC的平均晉升大小比目前老年代剩餘的空間大,則不會單獨觸發Young GC,而是轉爲觸發Full GC,也就是整堆的收集!

串行收集器

  串行垃圾收集器是最基本、發展歷史最悠久的收集器。主要包含Serial和Serrial Old兩種收集器,分別用來收集新生代和老年代。串行收集器因爲是單線程收集,在進行垃圾收集時,必須暫停(Stop The World)全部的工做線程,直到GC線程工做完成。運行示意圖以下:

  

  Serial 收集器:主要針對新生代回收,採用複製算法,單線程收集。
  Serial Old收集器:主要針對老年代回收,採用「標記-整理」算法,單線程收集。

  串行收集器在單CPU的環境下,沒有線程切換的開銷,能夠得到最高的單線程收集效率,可是因爲如今廣泛都是多CPU(或者多核)環境,因此除了在桌面應用中仍然將串行收集器做爲默認的收集器,其餘場景已經不多(不多不表明沒有,後面CMS會講到)使用。

  在上面咱們談到一個詞,須要暫停(Stop The World)全部的工做線程,這個概念在後面也會屢次提到,爲何須要暫停呢?一是爲了方便GC動做,否則在GC過程當中又會額外產生新的垃圾,或者分配新的對象。二是由於GC過程當中對象的地址會發生變化,若是不暫停線程,可能會致使引用出現問題。

並行收集器

  並行收集器是串行收集器的多線程版本,除了多線程外,其他的行爲、特色和串行收集器同樣。主要包含ParNew收集器、Parallel Scavenge收集器、Parallel Old收集器。運行示意圖以下:

  

  ParNew收集器:主要針對新生代回收,採用複製算法,多線程收集。通常老年代若是使用CMS收集器,則默認會使用ParNew做爲新生代收集器
  Parallel Scavenge收集器:該收集器與ParNew收集器相似,也是新生代收集器,採用複製算法,多線程收集。其餘收集器關注點是儘量地縮短垃圾收集時用戶線程停頓的時間,可是Parallel Scavenge收集器的目標則是達到一個可控的吞吐量(吞吐量=CPU運行用戶代碼時間/(CPU運行用戶代碼時間+CPU垃圾收集時間)),因此該收集器也成爲吞吐量收集器。因爲該收集器沒有使用傳統的GC收集器代碼框架,是另外獨立實現的,因此沒法和CMS收集器配合工做。
  Parallel Old收集器:主要針對老年代回收,採用「標記-整理」算法,多線程收集。該收集器是Parallel Scavenge收集器的老年代版本。在JDK1.6以後用來替代老年的Serial Old收集器。在注重吞吐量以及CPU資源敏感的場景,通常會選擇Parallel Scavenge+Parallel Old的組合進行垃圾收集。

CMS收集器

  前面介紹的幾種收集器都相對比較簡單,也很好理解,因此也沒作過多的介紹。接下來介紹的收集器相對前面幾種收集器就要複雜一些,而且使用較廣,因此介紹會較詳細!併發標記清理(Concurrent Mark Sweep)收集器也稱爲併發低停頓收集器或低延遲收集器。CMS收集器採用的是「標記-清理」算法,因此不會進行壓縮操做。咱們先來了解一下CMS收集器的運做過程:

  

  CMS收集器運做過程

  一、初始標記(CMS initial mark)
  僅標記GC Roots能直接關聯的對象,這個階段爲速度較快,可是仍然須要「Stop The World」,可是停頓時間較短!

  二、併發標記(CMS Concurrent mark)
  進行GC Roots Tracing的過程,也就是查找GC Roots能直接關聯的對象所引用的內存。在這個階段,GC線程與用戶線程是同時運行的,因此並不能保證能標記出全部存活的對象。

  三、從新標記(CMS remark)
  因爲併發標記階段,用戶線程在併發運行,因此可能在併發標記階段產生新的對象,因此在從新標記階段也會須要「Stop The World」來標記新產生的對象,且停頓時間比初始標記時間稍長,但遠比並發標記短。

  四、併發清除(CMS Concurrent sweep)
  在併發清除階段用戶線程與清理線程也是同時工做,清理線程回收全部的垃圾對象!

  CMS收集器缺點

  上面瞭解了CMS收集器的運做過程,不知道在瞭解過程當中你有沒有發現一些問題,好比CMS收集器採用的是「標記-清除」算法,那會不會產生不少的內存碎片?好比在併發清理階段,用戶線程還在運行,會不會在清理的過程當中又產生了垃圾?總結CMS收集器的幾個明顯的缺點以下:

  一、 對CPU資源很是敏感
  併發收集雖然不會暫停用戶線程,可是由於會佔用一部分CPU資源,仍是會致使應用程序變慢,總吞吐量降低。CMS的默認收集線程的數量=(CPU數量+3)/4。因此,當CPU數量大於4個時,會有超過25%的資源用於垃圾收集。當CPU數量小於或等於4個時,默認一個收集線程。

  二、 產生大量內存碎片
  CMS收集器採用「標記-清除」算法,在清除後不會進行壓縮操做,這樣會致使產生大量不連續的內存碎片,在分配大對象時,沒法找到足夠的連續內存,從而須要提早觸發一次FullGC的動做。針對該問題,提供了兩個參數來設置是否開啓碎片整理。
  1)、「-XX:+UseCMSCompactAtFullCollection」參數
  從名字能看出來,在收集的時候是否開啓壓縮。這個參數默認是開啓的,可是是否開啓壓縮還須要結合下面的參數!
  2)、「-XX:+CMSFullGCsBeforeCompaction」參數
  該參數設置執行多少次不壓縮的Full GC後,來一次壓縮整理。這個參數默認爲0,也就是說每次都執行Full GC,不會進行壓縮整理。
  若是開啓了壓縮,則在清理階段須要「Stop the world」,不能進行併發!

  三、 產生浮動垃圾
  上面說到過在併發清理階段,用戶線程還在運行,這時候可能就會又有新的垃圾產生,而沒法在這次GC過程當中被回收,這成爲浮動垃圾。

  四、 「Concurrent Mode Failure」失敗
  不知道你們在開發過程當中有沒有遇到過「Concurrent Mode Failure」失敗的信息,無論你有沒有遇到過,反正我是遇到過!這個異常是什麼緣由致使的呢。在併發標記和併發清除階段,用戶線程與GC線程併發工做,這會致使在清理的時候又會有用戶的線程在拼命的建立對象,自己垃圾回收時候確定是可用內存不夠了,可萬一這時候用戶線程建立了大量的對象怎麼辦呢?因此通常CMS收集器的垃圾回收的動做不會在徹底沒法分配內存的時候進行,能夠經過「-XX:CMSInitiatingOccupancyFraction」參數來設置CMS預留的內存空間!若是預留的空間沒法知足程序的須要,就會出現 「Concurrent Mode Failure」失敗。這時候JVM會啓用後備方案,也就是前面介紹過的Serial Old收集器,這樣會致使另外一次的Full GC的產生,這樣的代價是很大的,因此CMSInitiatingOccupancyFraction這個參數設置須要根據程序合理設置

  CMS收集器應用場景

  上面介紹了CMS收集器的缺點,那它固然也有它的優勢啦,好比並發收集、低停頓等等……因此CMS收集器適合與用戶交互較多的場景,注重服務的響應速度,能給用戶帶來較好的體驗!因此咱們在作WEB開發的時候,常常會使用CMS收集器做爲老年代的收集器!

相關文章
相關標籤/搜索