淺談JAVA虛擬機中的GC

前言

本文從JVM如何斷定對象是否須要回收開始分析,再到JVM的幾種垃圾回收思想如何產生,最後再來介紹JVM經典的7種垃圾回收器的特色(不包含ZGC);算法

JVM的分代思想

JVM根據對象存活週期不一樣將heap劃分紅了新生代、老年代、永久代(方法區&元空間)。
有個問題,JVM是先有的分代思想而後根據不一樣的代發展不一樣的垃圾回收思想,仍是先有的垃圾回收思想才劃分不一樣的代? 多線程

JVM如何判斷對象須要回收

JAVA與C有個很顯著的不一樣,就是JAVA不須要手動歸還內存,徹底由GC自動管理內存回收。那麼GC是如何判斷對象是否須要回收的呢?併發

  • 引用計數法性能

    引用計數法是指在對象中添加一個引用計數器,若是被其餘對象引用則計數器+1,引用失效時-1。
    優勢:實現簡單,判斷效率也很高;
    缺點:存在對象循環引用問題,因此在主流的虛擬機中並無採用引用計數器。
    對象A持有對象B的引用,對象B持有對象A的引用,除此以外在無其餘對象引用A和B,GC沒法回收這樣的對象.線程

  • 可達性分析3d

    在主流商用語言(JAVA/C#/Lisp)都是使用可達性分析算法來斷定對象是否存活。主要思想就是經過一系列被稱爲GC Roots的對象做爲起始點開始先下搜索,走過的路徑稱爲引用鏈,若是某個對象沒有任何一條到達GC Roots對象的引用鏈則表明此對象可回收的。
    JAVA中能夠被稱爲GC Roots對象:code

    • 虛擬機棧(棧幀中的本地變量表)中引用的對象;
    • 方法區中的類靜態屬性引用的對象;
    • 方法區中常量引用的對象;
    • 本地方法棧中JNI(即通常說的Native方法)中引用的對象;

    總結

    GC Roots沒法到達的對象並非必定會被回收,一個對象至少要被標記兩次纔會真正死亡。
    cdn

    • 第一次標記:當對象沒法關聯上GC Roots時會被第一次標記,並進行一次篩選,篩選的條件是該對象是否有必要執行finalize()
      條件一:對象是否有重寫finalize()方法
      條件二:虛擬機是否已經調用過該對象的finalize()方法
      篩選成功的對象會被放進一個隊列,稍後會由JVM自動建立的線程去執行finalize()方法;
    • 第二次標記:第二次標記時若是對象在finalize()方法裏關聯上任何一條引用鏈,則會被移出即將要回收的集合,不然該對象真正「死亡」。

GC的垃圾收集算法

在JVM知道那些對象是可回收的後,須要開始真正的回收對象了。JVM在發展的過程當中出現了幾種經典的回收思想,這裏不討論每種算法具體如何實現(由於我也不瞭解...)。對象

  • 標記-清除算法
    JVM分配內存時整個heap能夠看作一個大的表格裏有多個單元格,對於要回收的對象打上一個「標記」,而後對標記的對象進行「清除」,「標記-清除」也是最基礎的思想,後面的幾種思想都是基於這之上的改進。
    缺點:
    • 1.標記和清除過程效率都不高
    • 2.「標記-清除」後會產生大量不連續的內存碎片,當碰到須要分配較大對象內存時,沒法找到連續的空間則會觸發一次Young GC或者Full GC(兩種GC的區別能夠參考這篇文章 www.zhihu.com/question/41…)。

  • 複製算法
    爲了解決效率問題,出現了一種複製的算法,一開始是將內存按1:1劃分紅兩塊,每次只在其中一塊內存上分配對象,當觸發垃圾回收時將存活的對象所有複製到另外一塊的內存上,而後把已經使用過的那快內存清空掉。這樣既解決了效率問題也解決了內存碎片化的問題。但同時也帶來了空間浪費的缺點:每次只能使用50%的空間blog

    後來IBM有專門研究新生代的對象大多朝夕生死(建立後很快會銷燬),因此並不須要按1:1來分配,而是按8:1:1來劃分,一塊較大的Eden空間和兩塊較小的Survivor空間,每次分配佔用Eden+一塊Survivor空間(新對象的分配只會在Eden上),當垃圾回收時將存活的對象拷貝到另外一塊Survivor,這樣空間利用率達到90%。
    實際狀況並非每次回收時一塊Survivor都能裝下全部存活對象,那這時就會經過「空間分配擔保」的機制直接晉升到老年代。

  • 標記-整理算法
    因爲老年代的對象都是長期存活,因此複製算法並不適用老年代,所以又提出了「標記-整理」算法,標記過程與「標記-清除」算法同樣,只是後續並非直接清除對象而是先將全部存活對象都向一端移動,而後直接清理掉邊界之外的內存。

  • 分代收集算法
    當前主流商用垃圾回收器都是採用的「分代收集算法」,這個算法並無什麼新的思想只是根據對象存活週期的不一樣將內存劃分紅不一樣的代而後採用不一樣的回收算法。

    • 新生代的對象常規來講每次只有少許對象存活,若是用「標記」思想的話則效率和規則的過程都會很慢,故而採用「複製算法」。
    • 老年代對象大多存活量高,又沒有擔保空間,就必須採用「標記-清除」or「標記-整理」。

JVM中的垃圾收集器

黃色表明只處理新生代的GC,藍色表明只處理老年代GC,各GC之間的連線表明能夠搭配使用。G1能夠獨立回收整個head;

在介紹這些收集器各自的特性以前,讓咱們先來明確一個觀點:雖然咱們會對各個收集器進行比較,但並不是爲了挑選一個最好的收集器出來,雖然垃圾收集器的技術在不斷進步,但直到如今尚未最好的收集器出現,更加不存在「萬能」的收集器,因此咱們選擇的只是對具體應用最合適的收集器。若是有一種放之四海皆準、任何場景下都適用的完美收集器存在,HotSpot虛擬機徹底不必實現那麼多種不一樣的收集器了(摘選自《深刻理解Java虛擬機(第2版)》)。

這裏說明一下"並行"和"併發"的概念。

  • 並行(Parallel):多條垃圾收集線程並行工做,而此時用戶線程仍處於等待狀態。
  • 併發(Concurrent):垃圾收集線程與用戶線程同時執行(不必定是並行有多是交替執行),多核CPU的狀況下不一樣的線程在不一樣的CPU上同時執行。
  • Serial
    Serial收集器是最基本歷史最悠久的收集器,JDK1.3.1以前是新生代惟一的選擇。Serial是一個單線程收集器,這裏的「單線程」並非指一個CPU或一條線程而是Serial在垃圾收集時必須暫停其餘工做線程(Stop The World)也就是俗稱的「STW」。

    • 缺點:單線程,存在STW。
    • 優勢:簡單高效,單核CPU沒有線程交互的開銷,適合Client模式的虛擬機。
  • ParNew
    ParNew收集器是Serial收集器的多線程版本,除使用多條線程進行垃圾收集以外,其他行爲包括控制參數、收集算法、Stop The World、對象分配規則、回收策略等都與Serial收集器徹底同樣,在實現上,這兩種收集器也共用了至關多的代碼。

    • 缺點:存在STW,單核CPU性能比不會比Serial好。
    • 優勢:Server模式下首選的新生代收集器,除了Serial外目前只有它能與CMS搭配使用,多核CPU狀況下能有效利用系統資源。
  • Parallel Scavenge
    Parallel Scavenge收集器是一個並行的多線程年輕代收集器,其餘收集器關心如何縮短垃圾收集的時間而它關注的是如何控制系統運行的吞吐量(吞吐量(吞吐量 = 代碼運行時間 / (代碼運行時間 + 垃圾收集時間)))。高吞吐量能夠高效率的利用CPU時間,儘快完成運算任務,只要適合在後臺運算而不須要太多交互的任務。

    • 優勢:能夠精確控制吞吐量。
      • -XX:MaxGCPauseMillis用於控制最大垃圾收集停頓時間(一個大於0的整數,表明毫秒,收集器保證每次不超過這個時間,若是太小的話會頻繁發生GC,反而會下降吞吐量)
      • -XX:GCTimeRatio用於直接控制吞吐量的大小(是一個0-100之間的整數,表示應用程序運行時間和垃圾收集時間的比值。默認值爲99,即最大容許1%(1 / (1 + 99) = 1%)的垃圾收集時間)
      • -XX:UseAdaptiveSizePolicy虛擬機會根據當前系統的運行狀況動態調整合適的設置值來達到合適的停頓時間和合適的吞吐量,這種方式稱爲GC自適應調節策略。
    • 缺點:參數設置不當的狀況下可能會頻繁發生GC。
  • Serial Old
    Serial的老年代版本,它也是一款使用"標記-整理"算法的單線程的垃圾收集器,優劣和Serial同樣。有兩大用途:

    • JDK1.5前與Parallel Scavenge搭配使用
    • 做爲CMS發生Concurrent Mode Failure狀況下老年代預備方案
  • Parllel Old
    Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用"標記-整理"算法。JDK1.6才提供,在此以前Parallel Scavenge只能和單線程的Serial Old搭配使用,因爲老年代的Serial Old在服務端拖累又不能有效利用多核CPU的處理能力,致使Parallel Scavenge的高吞吐名副其實。直到Parllel Old的出現「吞吐量優先」的收集器纔有了用武之地,任何注重吞吐量以及CPU資源敏感的場合,均可以優先考慮Parallel Scavenge加Parallel Old收集器一塊兒配合使用

    • 優勢:能夠搭配Parallel Scavenge一塊兒使用,多線程收集老年代
  • CMS
    真正意義上的一款具備劃時代意義的垃圾收集器,基於「標記-清除」算法實現,關注點在獲取最短停頓時間爲目標,大量運用在B/S系統的服務端上。
    整個回收過程分爲四個步驟:

    • 初始標記(CMS initial mark)

      標記GC Roots直接關聯到的對象,速度很快。須要STW

    • 併發標記(CMS concurrent mark)

      標記GC Roots找到全部能關聯到的對象

    • 從新標記(CMS remark)

      由於併發標記是和用戶線程併發的因此在標記的過程當中會產生新的對象,因此要從新標記。須要STW

    • 併發清除(CMS concurrent sweep)

      併發清除前面全部標記的對象。

    • 優勢:
      • 併發收集:併發標記和併發清除兩個耗時階段是能夠和用戶線程併發執行的,而初始標記和從新標記耗時很短,因此基本上能夠認爲CMS在垃圾收集時是和應用程序併發執行的。
      • 低停頓
    • 缺點:
      • 對CPU資源敏感,併發階段會佔用部分CPU資源,致使程序變慢,吞吐量下降。
      • 沒法處理浮動垃圾(併發標記階段產生的垃圾只能下次回收處理),因此垃圾回收時要預留足夠的空間給用戶線程使用。
      • 由於採用「標記-清除」的算法,會產生大量空間碎片,從而致使老年代可能有很大空間剩餘可是卻沒法找到足夠大的連續空間分配大的對象,不得不提早觸發Full GC(Full GC以前也有可能觸發一次Young GC已下降Full GC的壓力)。
  • G1
    G1全稱「Garbage First」垃圾收集器直至JDK7,Sun公司才認爲G1達到足夠成熟的商用程度,目標是在將來能夠替換掉CMS。以前的GC都只負責整個新生代/老年代,而G1能夠獨立負責整個Heap,G1是將整個Heap劃分紅多個大小相等的Region,邏輯上仍保留分代的概念,但已不是物理分隔了,它們都是一部分不須要連續的Region集合。
    G1有如下特色:

    • 並行併發:可以充分利用多核多CPU來縮短STW時間,部分其餘GC須要用戶線程停頓的地方,G1能夠經過併發方式讓用戶線程繼續執行。
    • 分代收集:雖然物理上不在劃分新生代/老年代,可是分代思想仍在G1中保留,好比G1不須要搭配其餘收集器就能夠獨立管理整個Heap堆,可是G1會對存活週期不一樣的對象採用不一樣的方式去處理。
    • 空間整合:總體來看G1採用「標記-整理」算法實現,從局部來看兩個Region之間是基於「複製」算法實現的。無論怎麼說G1不會像CMS同樣出現內存碎片問題。
    • 可預測的停頓:G1除了追求低停頓之外,還能創建可預測的停頓時間模型(指定一個長度爲M毫秒的時間段內,消耗在垃圾收集上的時間不超過N毫秒)。G1對每個Region的垃圾堆積的價值大小維護了一個優先列表,每次根據容許的收集時間,優先回收價值大的Region(這就是Garbage First名稱的由來),保證了有限的時間內獲取儘量高的收集效率。

    G1收集器的大概步驟:

    • 初始標記
    • 併發標記
    • 最終標記
    • 篩選回收:對各個Region的回收價值和成本進行排序,而後根據用戶指望的停頓時間來制定回收計劃。

    前三個步驟與CMS運做過程大體類似,

相關文章
相關標籤/搜索