JVM—【02】認識JVM的垃圾回收算法與收集器

1. 對象存活判斷

1.1. 引用計數算法 Reference Counting

  • 給對象添加一個引用計數器,每當有一個地方引用它的時候,計數器值就加一;當引用失效時,計數器值就減一;任什麼時候刻計數器爲0的對象就是不可能再被使用的。
  • 主流的JVM沒有選用引用計數算法來管理內存,主要的緣由是它很難解決對象之間的相互循環引用的問題。

1.2. 可達性分析算法 Reachability Analysis

  • 經過一系列稱爲「GC-Roots」的對象做爲起點,從這些結點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證實此對象是不可用的(圖論中的不可達)。
  • 可做爲GC Roots的對象:

虛擬機棧(戰爭中的本地變量表)中引用的對象算法

方法區中類靜態屬性引用的對象安全

方法區中常量引用的對象數據結構

本地方法棧中JNI引用的對象多線程


1.3. 引用類型 Reference

  • 強引用:Strong Reference

指的是相似於Object object = new Object()這類引用,只要強引用存在,垃圾收集器就永遠不會回收被引用對象。併發

  • 軟引用:Soft Reference

描述一些還有用但並不是必要的對象。JDK提供了SoftReference來實現軟引用微服務

在系統快要發生內存溢出以前,將會把這些對象列進回收範圍之中進行第二次回收。若是此次回收尚未足夠的內存,纔會拋出內存溢出異常。佈局

  • 弱引用: Weak Reference

用來描述非必須對象,它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生以前。JDK提供了WeakReference類來實現弱引用。性能

當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。學習

  • 虛引用:Phantom Reference

也稱爲幽靈引用或幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。JDK提供PhantomReference類來實現虛引用大數據

爲一個對象設置虛引用關聯的惟一目的就是能在這個對象被收集器回收時收到一個系統通知


1.3. 引用類型 Reference

  • 不可達對象,會暫時處於「緩刑」階段,要真正宣告一個對象死亡,至少要經歷兩次標記過程:

    若是對象在進行可達性分析後發現沒有與GC Roots相鏈接的引用鏈,那它將會被第一次標記而且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種狀況都視爲「沒有必要執行」。

    finalize()方法是對象逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模的標記,若是對象要在finalize()中成功拯救本身——只要從新與引用鏈上的任何一個對象創建關聯便可,譬如把本身(this關鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移除出「即將回收」的集合;若是對象這時候尚未逃脫,那基本上它就真的被回收了。

    注:若是對象唄斷定有必要執行finalize()方法,那麼這個對象將會放置在一個叫作F-Queue隊列中,並隨後JVM會建立一個低優先級的Finalizer線程去執行它。JVM觸發這個方法,並不確保它會執行結束,由於若是對象finalize方法若是執行緩慢或者死循環,將頗有可能會致使F-Queue隊列其餘對象永久等待,甚至致使整個內存回收系統奔潰。


2. 垃圾收集算

2.1. 標記-清除算法 Mark-Sweep

  • 算法分兩個階段,即標記和清除。

    1. 標記處所須要回收的對象
    1. 標記完成後統一回收全部被標記對象
  • 算法主要不足

    1. 效率問題,標記和清除兩個過程效率都不高
    2. 空間問題,標記清除後悔產生大量不連續的空間

    空間碎片太多可能會致使之後分配大對象時沒法找到足夠連續內存存放而不得不觸發另外一次垃圾收集。


2.2. 複製算法 Copying

  • 將可用的內存按照容量劃分爲大小相等的兩塊,每次使用其中一塊。當前一塊用完了,將還存活的對象移動到另外一塊上面,而後把已使用過的內存空間一次性清理掉。這樣每次都是堆整個半區進行內存回收,分配內存時也就不考慮內存碎片等複雜狀況,實現簡單、運行高效。代價是將內存縮小爲原來的一半。

2.3. 標記-整理算法 Mark-Compact

  • 標記後不直接對可回收對象清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界覺得的內存。

2.4. 分代收集算法 Generational Collection

  • 把JVM堆內存分爲新生代和老年代,對不一樣的年代採起不一樣的收集算法。

    在新生代中每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法。

    老年代中由於對象存活率高、沒有額外空間對它進行分配擔保,那就必須使用「標記-清理」或「標記-整理」算法來進行回收。


3. 垃圾收集算

3.1. 枚舉根節點

  • 可達性分析從GC Roots節點找引用鏈操做,如今引用僅方法區就有數百兆,逐個檢查裏面的引用很是耗時。
  • 可達性分析對執行時間的敏感上體如今GC停頓上,這項分析工做必須在一個能確保一致性的快照中,

    這裏的一致性是指在整個分析期間整個執行系統開起來像被凍結在某個時間節點上。若是這點不知足準確性就沒法保證。這是致使GC進行時必須停頓全部Java執行線程的其中一個重要緣由。即便在CMS收集器(號稱幾乎不發生停頓)中枚舉根節點也是必需要停頓的。

  • 主流的JVM都是使用的準確式GC,因此當執行系統停頓下來並不須要一個不漏檢查完全部執行上下文和全局的引用位置,JVM知道哪裏存放這個信息,在HotSpot使用了一組OopMap的數據結構來達到這個目的。

    在類加載完後,HotSpot吧對象內的各個偏移量上的類型計算出來,在JIT編譯過程當中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。在GC掃描時,就能夠直接知道這些信息。


3.2. 安全點 Safepoint

  • HotSpot在特定的位置記錄棧和寄存器中哪些位置是引用,這個「特定位置」就稱爲「安全點」,即程序執行時並不是在全部地方都能停頓下來開始GC,只有在到達安全點時才能暫停。

  • 安全點不能太多,也不能太少,太多增大系統負荷,太少GC等待時間太長。因此安全點的選擇基本是以「是否具備讓程序長時間執行的特徵」爲標準選定。

    由於每條指令執行時間都很是短暫,程序不太可能由於指令流長度太長而過長時間運行,因此長時間的特徵就是指令序列複用循環跳轉異常跳轉

  • 怎樣確保GC發生全部線程都跑到安全點再停頓下來,有兩種方案:

    搶先式中斷(Preemptive Suspension):在發生GC時,首先把全部線程所有中斷,若是發現有線程中斷的地方不在安全點上,就恢復線程,讓它跑到安全點上。

    主動式中斷(Voluntary Suspension):當GC須要中斷線程時,不對線程直接操做,僅簡單設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真的時候就本身把中斷掛起。輪詢標誌這個地方和安全點是重合的,另外再加上建立對象須要分配內存的地方。


3.3. 安全區域 Safe Region

  • 安全區域是指一段代碼片斷中,引用關係不會發發生變化。在這個區域中的任意地方開始GC都是安全的。
  • 在線程執行到Safe Region中的代碼時,首先表示本身進入了Safe Region,這這段時間裏,JVM要發起GC時,就不用管標識本身爲Safe Region狀態的線程了。在線程要離開Safe Region時,它要檢查系統是否已經完成了根節點枚舉(或者整個GC過程),若是完成了,那線程就繼續執行,不然它就必須等待知道收到能夠安全離開Safe Region的信號爲止。

4. 垃圾收集器

4.1. Serial收集器

  • 是一個單線程垃圾收集器,它只會使用一個CPU或者一條收集線程去完成垃圾收集工做。
  • 它在進行垃圾收集時,必須暫停其餘全部的工做線程,知道收集結束。
  • 適用於Client。
  • 新生代使用複製算法,暫停全部線程;老年代使用標記-整理算法,暫停全部線程。

4.2. ParNew收集器

  • Serial的多線程版本,除了使用多條線程進行垃圾收集以外,其他行爲包括Serial收集器的可用參數、收集算法、Stop The World、對象分配規則、回收策略都與Serial收集器徹底同樣

  • 除了Serial收集器外,目前只有它能與CMS收集器配合工做。

    ParNew收集器也是使用-XX:+UseConcMarkSweepGC選項後的默認新生代收集器,也可使用-XX:+UseParNewGC選項強制指定。

    ParNew在單核下不會比Serial收集器效果好

    可使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。


4.3. Parallel Scavenge收集器

  • 他是一個新生代處理器,也是使用複製算法的收集器,也是並行的多線程處理器。

  • Parallel Scavenge收集器的目的是達到一個可控制的吞吐量。

    吞吐量 = 運行用戶代碼的時間 / (運行用戶代碼時間 + 垃圾收集時間)

  • 它提供了兩個參數控制吞吐量:控制最大垃圾收集停頓的時間-XX:MaxGCPauseMillis,直接設置吞吐量大小-XX:GCTimeRatio

    -XX:MaxGCPauseMillis:容許的值是一個大於0的毫秒數,收集器將盡量地保證內存回收花費不超過設定值,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的。

    -XX:GCTimeRatio:參數的值是大於0小於100的整數,就是垃圾收集時間佔總時間的比率,如:19,容許最大的時間就是1/(1+19);99,容許最大的時間就是1/(1+99)

  • Parallel Scavenge參數:-XX:UseAdaptiveSizePolicy

    -XX:UseAdaptiveSizePolicy 打開這個參數,就不須要手工指定新生代大小、Eden與Survivor區的比列、晉升老年代對象大小等細節參數。虛擬機會根據當前系統的運行狀況收集性能監控,動態調整這些參數以提供最適合的停頓時間和最大吞吐量,這種調節方式稱爲GC自適應調整策略(GC Ergonomics)


4.4. Serial Old收集器

  • Serial收集器的老年代版,單線程,使用「標記-整理」算法
  • 做爲CMS收集器的後背元,在併發收集發生Concurrent Mode Failure時使用。

4.5. Parallel Old收集器

  • 是Parallel Scavenge收集器的老年代版本。使用多線程和「標記-整理」算法。
  • 在注重吞吐量以及CPU資源敏感的場景,能夠優先考慮Paralled Scavenge+Parallel Old收集器。

4.6 CMS(Concurrent Mark Swap) 收集器

  • CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。適用於互聯網站和B/S系統的服務端上。併發收集、低停頓。

  • CMS收集器是基於「標記-清除」算法實現,過程分爲4步:

    初始標記(CMS initial mark):僅僅是標記一下GC Roots能直接關聯到的對象,速度很快。

    併發標記(CMS concurrent mark):進行GC Roots Tracing的過程。

    從新標記(CMS remark):是爲了修正併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄。停頓時間通常比初始標記更長,遠比並發標記時間短。

    併發清除(CMS concurrent sweep)

    其中初始標記從新標記這兩個步驟仍然須要「stop the world」

  • CMS的幾個缺點:

    對CPU資源很是敏感:它雖然不會致使用戶線程停頓,可是啓用線程,消耗CPU運算資源,會致使引用程序變慢,總吞吐量下降。CMS的默認啓用回收的線程數是(CPU數量 + 3)/ 4.也就是說,CPU越少,佔用性能越多,對程序的影響就越大。爲了應對這種情況,JVM提供了「增量式併發收集器」(Incremental Concurrent Mark Swap/i-CMS),使用搶佔式來模擬多任務機制,在併發標記和清理的時候讓GC線程、用戶線程交替運行。儘可能減小GC線程獨佔資源的時間,這樣整個垃圾收集時間過程會更長,可是對用戶的影響就顯得更少。

    CMS沒法處理「浮動垃圾(Floating Garbage)」,可能出現「Concurrent Mode Failure」失敗而致使另外一次Full GC的產生。浮動垃圾即在CMS併發清理時用戶線程還在運行產生的心垃圾,這部分垃圾出如今標記事後,沒法再當次處理。正由於用戶線程還在運行,就須要預留一部份內存給用戶線程使用,因此CMS能夠設置觸發百分比:-XX:CMSInitiatingOccupancyFraction=70-XX:+UseCMSInitiatingOccupancyOnly 前者設置百分比,後者設置只用設置的百分比,不讓JVM自動調整,若是不設置後面的,第一次會使用70,隨後就會隨JVM自動調整了。若是CMS運行時,預留內存沒法知足須要,就會出現「Concurrent Mode Failure」,這是JVM就會啓用後後備方案使用Serial Old來從新進行老年代收集。因此比例不能設置過高,否則就會容易引發Concurrent Mode Failure,性能反而下降。

    CMS是基於「標記-清除」算法實現的,因此收集結束後會有大量的空間碎片產生。雖然空間不少,可是沒法給大對象找到一片連續的空間,從而不得不觸發一次Full GC。爲了解決這個問題,CMS提供了一個-XX:+UseCMSCompactAtFullCollection,用於在CMS要進行Full GC的時候開啓內存碎片合併整理,這個過程沒法併發進行,空間碎片問題解決,可是停頓時間變長。CMS還有一個-XX:CMSFullGCsBeforeCompaction,這個參數是用於設置執行多少次不壓縮的Full GC後,跟着來一次帶壓縮的,爲0是表示每次進入Full GC 都壓縮。


4.7 G1(Garbage-First)收集器

  • G1是一款面向服務端應用的垃圾收集器。HotSpot開發來替代CMS的,特色以下:

    並行與併發: G1能充分利用多CPU、多核環境下的硬件優點,使用多個CPU來縮短Stop-The-World停頓的時間,部分其餘收集器本來須要停頓Java線程執行的GC動做,G1收集器仍然能夠經過併發的方式讓Java程序繼續執行。

    分代收集: 分代概念在G1中依然得以保留。G1能夠不須要其餘收集器配合就能獨立管理整個GC堆,它可以採用不一樣的方式去處理新建立的對象和已經存活了一段時間、熬過屢次GC的舊對象以獲取更好的收集效果。G1能夠本身管理新生代和老年代。

    可預測的停頓: 下降停頓時間是G1和CMS共同的關注點,G1除了追求低停頓外,還創建可預測的停頓時間模型,能讓使用者明確指定在一個長度爲M毫秒的時間片斷內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已是實時Java(RTSJ)的垃圾收集器的特徵了。G1能夠有計劃的避免在整個JVM堆中進行垃圾收集,能夠對每一個region裏的回收對象價值(回收該區域的時間消耗和能獲得的內存比值)進行分析,在最後篩選回收階段,對每一個region裏的回收對象價值(回收該區域的時間消耗和能獲得的內存比值)最後進行排序,用戶能夠自定義停頓時間,那麼G1就能夠對部分的region進行回收!這使得停頓時間是用戶本身能夠控制的!

    空間整合,沒有內存碎片產生:因爲G1使用了獨立區域(Region)概念,G1從總體來看是基於「標記-整理」算法實現收集,從局部(兩個Region)上來看是基於「複製」算法實現的,但不管如何,這兩種算法都意味着G1運做期間不會產生內存空間碎片。

  • 在G1以前的其餘收集器進行收集的範圍都是整個新生代或者老年代,而G1再也不是這樣。使用G1收集器時,Java堆的內存佈局就與其餘收集器有很大差異,它將整個Java堆劃分爲多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔離的了,它們都是一部分Region(不須要連續)的集合。

  • G1收集器之因此能創建可預測的停頓時間模型,是由於它能夠有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收所得到的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值最大的Region(這也就是Garbage-First名稱的來由)。這種使用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收集器的運做大體可劃分爲如下幾個步驟:

    初始標記(Initial Marking)

    併發標記(Concurrent Marking)

    最終標記(Final Marking)

    篩選回收(Live Data Counting and Evacuation)


關於我

  • 座標杭州,普通本科在讀,計算機科學與技術專業,20年畢業,目前處於實習階段。
  • 主要作Java開發,會寫點Golang、Shell。對微服務、大數據比較感興趣,預備作這個方向。
  • 目前處於菜鳥階段,各位大佬輕噴,小弟正在瘋狂學習。
  • 歡迎你們和我交流鴨!!!
相關文章
相關標籤/搜索