JVM垃圾回收機制入門

如何斷定對象爲垃圾對象

在堆裏面存放着Java世界中幾乎全部的對象實例, 垃圾收集器在對堆進行回收前, 第一件事就是判斷哪些對象已死(可回收).html

引用計數法

在JDK1.2以前,使用的是引用計數器算法。 在對象中添加一個引用計數器,當有地方引用這個對象的時候,引用計數器的值就+1,當引用失效的時候,計數器的值就-1,當引用計數器被減爲零的時候,標誌着這個對象已經沒有引用了,能夠回收了!java

**問題:**若是在A類中調用B類的方法,B類中調用A類的方法,這樣當其餘全部的引用都消失了以後,A和B還有一個相互的引用,也就是說兩個對象的引用計數器各爲1,而實際上這兩個對象都已經沒有額外的引用,已是垃圾了。可是該算法並不會計算出該類型的垃圾。

可達性分析法

在主流商用語言(如Java、C#)的主流實現中, 都是經過可達性分析算法來斷定對象是否存活的: 經過一系列的稱爲 GC Roots 的對象做爲起點, 而後向下搜索; 搜索所走過的路徑稱爲引用鏈/Reference Chain, 當一個對象到 GC Roots 沒有任何引用鏈相連時, 即該對象不可達, 也就說明此對象是不可用的, 以下圖:雖然E和F相互關聯, 但它們到GC Roots是不可達的, 所以也會被斷定爲可回收的對象。 web

注: 即便在可達性分析算法中不可達的對象, VM也並非立刻對其回收, 由於要真正宣告一個對象死亡, 至少要經歷兩次標記過程: 第一次是在可達性分析後發現沒有與GC Roots相鏈接的引用鏈, 第二次是GC對在F-Queue執行隊列中的對象進行的小規模標記(對象須要覆蓋finalize()方法且沒被調用過).算法

在Java, 可做爲GC Roots的對象包括:
  1. 方法區: 類靜態屬性引用的對象;
  2. 方法區: 常量引用的對象;
  3. 虛擬機棧(本地變量表)中引用的對象.
  4. 本地方法棧JNI(Native方法)中引用的對象。

如何回收

回收策略

垃圾收集策略有分代收集和分區收集。多線程

分代收集算法

標記-清除算法(老年代)

該算法分爲「標記」和「清除」兩個階段: 首先標記出全部須要回收的對象(可達性分析), 在標記完成後統一清理掉全部被標記的對象. 併發

該算法會有兩個問題:oracle

  1. 效率問題,標記和清除效率不高。
  2. 空間問題: 標記清除後會產生大量不連續的內存碎片, 空間碎片太多可能會致使在運行過程當中須要分配較大對象時沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集。

因此它通常用於"垃圾不太多的區域,好比老年代"。框架

複製算法(新生代)

該算法的核心是將可用內存按容量劃分爲大小相等的兩塊, 每次只用其中一塊, 當這一塊的內存用完, 就將還存活的對象(非垃圾)複製到另一塊上面, 而後把已使用過的內存空間一次清理掉.jvm

優勢:不用考慮碎片問題,方法簡單高效。 缺點:內存浪費嚴重。線程

現代商用VM的新生代均採用複製算法, 但因爲新生代中的98%的對象都是生存週期極短的, 所以並不需徹底按照1∶1的比例劃分新生代空間, 而是將新生代劃分爲一塊較大的Eden區和兩塊較小的Survivor區(HotSpot默認Eden和Survivor的大小比例爲8∶1), 每次只用Eden和其中一塊Survivor. 當發生MinorGC時, 將Eden和Survivor中還存活着的對象一次性地拷貝到另一塊Survivor上, 最後清理掉Eden和剛纔用過的Survivor的空間. 當Survivor空間不夠用(不足以保存尚存活的對象)時, 須要依賴老年代進行空間分配擔保機制, 這部份內存直接進入老年代。

複製算法的空間分配擔保: 在執行Minor GC前, VM會首先檢查老年代是否有足夠的空間存放新生代尚存活對象, 因爲新生代使用複製收集算法, 爲了提高內存利用率, 只使用了其中一個Survivor做爲輪換備份, 所以當出現大量對象在Minor GC後仍然存活的狀況時, 就須要老年代進行分配擔保, 讓Survivor沒法容納的對象直接進入老年代, 但前提是老年代須要有足夠的空間容納這些存活對象. 但存活對象的大小在實際完成GC前是沒法明確知道的, 所以Minor GC前, VM會先首先檢查老年代連續空間是否大於新生代對象總大小或歷次晉升的平均大小, 若是條件成立, 則進行Minor GC, 不然進行Full GC(讓老年代騰出更多空間). 然而取歷次晉升的對象的平均大小也是有必定風險的, 若是某次Minor GC存活後的對象突增,遠遠高於平均值的話,依然可能致使擔保失敗(Handle Promotion Failure, 老年代也沒法存放這些對象了), 此時就只好在失敗後從新發起一次Full GC(讓老年代騰出更多空間).


標記-整理算法(老年代)

標記清除算法會產生內存碎片問題, 而複製算法須要有額外的內存擔保空間, 因而針對老年代的特色, 又有了標記整理算法. 標記整理算法的標記過程與標記清除算法相同, 但後續步驟再也不對可回收對象直接清理, 而是讓全部存活的對象都向一端移動,而後清理掉端邊界之外的內存.

方法區回收(永久代)

在方法區進行垃圾回收通常」性價比」較低, 由於在方法區主要回收兩部份內容: 廢棄常量無用的類.

回收廢棄常量與回收其餘年代中的對象相似, 但要判斷一個類是否無用則條件至關苛刻:

  1. 該類全部的實例都已經被回收, Java堆中不存在該類的任何實例;
  2. 該類對應的Class對象沒有在任何地方被引用(也就是在任何地方都沒法經過反射訪問該類的方法);
  3. 加載該類的ClassLoader已經被回收. 但即便知足以上條件也未必必定會回收, Hotspot VM還提供了-Xnoclassgc參數控制(關閉CLASS的垃圾回收功能). 所以在大量使用動態代理、CGLib等字節碼框架的應用中必定要關閉該選項, 開啓VM的類卸載功能, 以保證方法區不會溢出.

分區收集

分區算法則將整個堆空間劃分爲連續的不一樣小區間, 每一個小區間獨立使用, 獨立回收. 這樣作的好處是能夠控制一次回收多少個小區間

在相同條件下, 堆空間越大, 一次GC耗時就越長, 從而產生的停頓也越長. 爲了更好地控制GC產生的停頓時間, 將一塊大的內存區域分割爲多個小塊, 根據目標停頓時間, 每次合理地回收若干個小區間(而不是整個堆), 從而減小一次GC所產生的停頓

垃圾回收器

Serial

Serial收集器是Hotspot運行在Client模式下的默認新生代收集器, 它在進行垃圾收集時,會暫停全部的工做進程,用一個線程去完成GC工做

特色:簡單高效,適合jvm管理內存不大的狀況(十兆到百兆)。

Parnew

ParNew收集器實際上是Serial的多線程版本,回收策略徹底同樣,可是他們又有着不一樣。

咱們說了Parnew是多線程gc收集,因此它配合多核心的cpu效果更好,若是是一個cpu,他倆效果就差很少。(可用-XX:ParallelGCThreads參數控制GC線程數)

Cms

CMS(Concurrent Mark Sweep)收集器是一款具備劃時代意義的收集器, 一款真正意義上的併發收集器, 雖然如今已經有了理論意義上表現更好的G1收集器, 但如今主流互聯網企業線上選用的還是CMS(如Taobao),又稱多併發低暫停的收集器。

由他的英文組成能夠看出,它是基於標記-清除算法實現的。整個過程分4個步驟:

  1. 初始標記(CMS initial mark):僅只標記一下GC Roots能直接關聯到的對象, 速度很快
  2. 併發標記(CMS concurrent mark: GC Roots Tracing過程)
  3. 從新標記(CMS remark):修正併發標記期間因用戶程序繼續運行而致使標記產生變更的那一部分對象的標記記錄
  4. 併發清除(CMS concurrent sweep: 已死對象將會就地釋放)

能夠看到,初始標記、從新標記須要STW(stop the world 即:掛起用戶線程)操做。由於最耗時的操做是併發標記和併發清除。因此整體上咱們認爲CMS的GC與用戶線程是併發運行的。

**優勢:**併發收集、低停頓

缺點:

  1. CMS默認啓動的回收線程數=(CPU數目+3)*4 當CPU數>4時, GC線程最多佔用不超過25%的CPU資源, 可是當CPU數<=4時, GC線程可能就會過多的佔用用戶CPU資源, 從而致使應用程序變慢, 總吞吐量下降.
  2. 沒法清除浮動垃圾(GC運行到併發清除階段時用戶線程產生的垃圾),由於用戶線程是須要內存的,若是浮動垃圾施放不及時,極可能就形成內存溢出,因此CMS不能像別的垃圾收集器那樣等老年代幾乎滿了才觸發,CMS提供了參數-XX:CMSInitiatingOccupancyFraction來設置GC觸發百分比(1.6後默認92%),固然咱們還得設置啓用該策略-XX:+UseCMSInitiatingOccupancyOnly
  3. 由於CMS採用標記-清除算法,因此可能會帶來不少的碎片,若是碎片太多沒有清理,jvm會由於沒法分配大對象內存而觸發GC,所以CMS提供了-XX:+UseCMSCompactAtFullCollection參數,它會在GC執行完後接着進行碎片整理,可是又會有個問題,碎片整理不能併發,因此必須單線程去處理,因此若是每次GC完都整理用戶線程stop的時間累積會很長,因此XX:CMSFullGCsBeforeCompaction參數設置隔幾回GC進行一次碎片整理(默認爲0)。
G1

同優秀的CMS垃圾回收器同樣,G1也是關注最小時延的垃圾回收器,也一樣適合大尺寸堆內存的垃圾收集,官方也推薦使用G1來代替選擇CMS。G1最大的特色是引入分區的思路,弱化分代的概念,合理利用垃圾收集各個週期的資源,解決了其餘收集器甚至CMS的衆多缺陷。

由於每一個區都有E、S、O代,因此在G1中,不須要對整個Eden等代進行回收,而是尋找可回收對象比較多的區,而後進行回收(雖然也須要STW操做,可是花費的時間是不多的),保證高效率。

新生代收集

G1的新生代收集跟ParNew相似,若是存活時間超過某個閾值,就會被轉移到S/O區。

年輕代內存由一組不連續的heap區組成, 這種方法使得能夠動態調整各代區域的大小

老年代收集

分爲如下幾個階段:

  1. 初始標記 (Initial Mark: Stop the World Event) 在G1中, 該操做附着一次年輕代GC, 以標記Survivor中有可能引用到老年代對象的Regions.
  2. 掃描根區域 (Root Region Scanning: 與應用程序併發執行) 掃描Survivor中可以引用到老年代的references. 但必須在Minor GC觸發前執行完
  3. 併發標記 (Concurrent Marking : 與應用程序併發執行) 在整個堆中查找存活對象, 但該階段可能會被Minor GC中斷
  4. 從新標記 (Remark : Stop the World Event) 完成堆內存中存活對象的標記. 使用snapshot-at-the-beginning(SATB, 起始快照)算法, 比CMS所用算法要快得多(空Region直接被移除並回收, 並計算全部區域的活躍度).
  5. 清理 (Cleanup : Stop the World Event and Concurrent) 在含有存活對象和徹底空閒的區域上進行統計(STW)、擦除Remembered Sets(使用Remembered Set來避免掃描全堆,每一個區都有對應一個Set用來記錄引用信息、讀寫操做記錄)(STW)、重置空regions並將他們返還給空閒列表(free list)(Concurrent)

詳情請看參考文檔

相關文章
相關標籤/搜索