JVM內存管理---垃圾收集器

提及垃圾收集(Garbage Collection,GC),大部分人都把這項技術當作Java語言的伴生產物。事實上,GC的歷史遠比Java久遠,1960年誕生於MIT的Lisp是第一門真正使用內存動態分配和垃圾收集技術的語言。當List還在胚胎時期時,人們就在思考GC須要完成的3件事情:算法

  • 哪些內存須要回收?
  • 何時回收?
  • 如何回收?

1、哪些內存須要回收?安全

從JVM區域結構看,可將這些區域劃分爲「靜態內存」和「動態內存」兩類。程序計數器、虛擬機棧、本地方法3個區域是「靜態」的,由於這幾個區域的內存分配和回收都具有肯定性,都隨着線程而生,隨着線程而滅。但Java堆和方法區不同,內存分配都存在不肯定性,只有在程序處於運行期間才能知道會建立哪些對象,這部份內存和回收都是動態的,垃圾收集器所關注的是這部份內存。多線程

在堆裏面存放着Java世界幾乎全部的對象實例,垃圾回收器在對堆進行回收前,第一件事情就是就是要肯定這些對象哪些還"存活"着,哪些已經"死去"。那麼又怎麼肯定對象已經"死去"呢?併發

1.引用計數法:測試

分配對象時給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任什麼時候刻計數器爲0的對象就是沒有再被使用了。客觀地說,引用計數法(Reference Counting)的實現簡單,判斷效率也很高,可是在主流的Java虛擬機裏面沒有選用引用計數法來管理內存,其中最主要的緣由是它很難解決對象之間相互循環引用的問題。例如:線程

public class ReferenceCountingGC {

    public Object instance = null;
    private byte[] bigsize = new byte[2*1024*1024];

    public static void testGC(){
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        
        objA.instance = objB;
        objB.instance = objA;
        
        objA = null;
        objB = null;
        
        System.gc();
    }
}

當設置objA = null;objB = null後這兩個對象再無任何引用,實際上這兩個對象已經不可能再被訪問,可是它們由於互相引用着對方,致使它們的引用計數都不爲0,因而引用計數算法沒法通知GC收集器回收它們。若是這個對象特別大,則會形成嚴重的內存泄露。設計

2.可達性分析算法:調試

可達性分析(Reachability Analysis)的基本思想是經過一系列的稱爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時(也就是GC Roots到這個對象不可達),則證實此對象是不可用的。以下圖所示:code

對象Object五、Object六、Object7相互雖然有關聯,可是它們到GC Roots是不可達的,因此它們將會被斷定爲是可回收的對象。在Java語言中,可做爲GC Roots的對象包括下面幾種:對象

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

2、何時回收?

虛擬機爲了分析GC Roots這項工做必須在一個能確保一致性的快照中進行,這裏的「一致性」的意思就是指在整個分析期間整個執行系統看起來就像被凍結在某個時間點上——這叫安全點。固然,程序執行時並不是在全部地方都能停頓下來開始GC,只有到達安全點時才能暫停。安全點選址也有規定的,選定基本上是以程序「是否具備讓程序長時間執行的特徵」爲標準進行選定的。這裏的長時間執行的最明顯特徵是指令列複用,例如方法調用、循環跳轉、異常跳轉等。

虛擬機爲了能讓全部線程都「跑」到安全點上停頓下來,設計了兩個方案:搶先式中斷和主動式中斷。其中搶先式中斷是虛擬機發生GC時,首先把全部線程所有中斷,若是發生有線程中斷的地方不在安全點上,就恢復線程,讓它「跑」到安全點上。這種方式如今比較用了。而主動式中斷是虛擬機須要GC時僅僅簡單的設置一個標誌,各個線程執行到安全點時主動去輪詢這個標誌,發現中斷標誌爲真時就本身中斷掛起。

3、如何回收?

3.1 垃圾收集算法:

(1)標記-清除(Mark-Sweep)算法

這是最基礎的算法,就像它名字同樣,算法分爲「標記」和「清除」兩個階段:首先標記處全部須要回收的對象(如哪些內存須要回收所描述的對象),對標記完成後統一回收全部被標記的對象,以下圖所示:

缺點:一個是效率問題,標記和清除兩個過程的效率都不高;另外一個是空間問題,標記清除後悔產生大量的不連續的內存碎片,可能會致使後續沒法分配大對象而致使再一次觸發垃圾收集動做。

(2)複製算法

爲了針對標記-清除算法的不足,複製算法將可用內存容量劃分爲大小相等的兩塊,每次只使用一塊。當一塊的內存用完了,就將還存活的對象複製到另外一塊上面去。而後把已使用過的內存空間一次清理掉,以下圖所示:

缺點:使用內存比原來縮小了一半。

如今的商業虛擬機都採用這種收集算法來回收新生代,有企業分析的得出其實並不需求將內存按1:1的比例劃分,由於新生代中的對象大部分都是「朝生夕死」的。因此,HotSpot虛擬機默認的Eden和Survivor的大小比例是8:1。一塊Eden和兩塊Survivor,每次使用一塊Eden和一塊Survivor,也就是說只有10%是浪費的。若是另外一塊Survivor都沒法存放上次垃圾回收的對象時,那這些對象將經過「擔保機制」進入老年代了。

(3)標記-整理(Mark-Compact)算法
複製算法通常是對對象存活率較低的一種回收操做,但對於對象存活率較高的內存區域(老年代)來講,效果就不是那麼理想了,標記-整理算法所以誕生了。標記-整理算法和標記-清除算法差很少,都是一開始對回收對象進行標記,但後續不是直接對對象清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存,以下圖所示:

(4)分代收集算法

分代收集算法是目前大部分JVM的垃圾收集器採用的算法。它的核心思想是根據對象存活的生命週期將內存劃分爲若干個不一樣的區域。通常狀況下將堆區劃分爲老年代(Tenured Generation)和新生代(Young Generation),老年代的特色是每次垃圾收集時只有少許對象須要被回收,而新生代的特色是每次垃圾回收時都有大量的對象須要被回收,那麼就能夠根據不一樣代的特色採起最適合的收集算法。

3.2 垃圾收集器:

(1)七種垃圾收集器:

  • Serial(串行GC)-複製
  • ParNew(並行GC)-複製
  • Parallel Scavenge(並行回收GC)-複製
  • Serial Old(MSC)(串行GC)-標記-整理
  • CMS(併發GC)-標記-清除
  • Parallel Old(並行GC)--標記-整理
  • G1(JDK1.7update14才能夠正式商用)

說明:

  • 1~3用於年輕代垃圾回收:年輕代的垃圾回收稱爲minor GC
  • 4~6用於年老代垃圾回收(固然也能夠用於方法區的回收):年老代的垃圾回收稱爲full GC
  • G1獨立完成"分代垃圾回收"

注意:並行與併發

  • 並行:多條垃圾回收線程同時操做
  • 併發:垃圾回收線程與用戶線程一塊兒操做

(2)經常使用五種組合:

  • Serial/Serial Old
  • ParNew/Serial Old:與上邊相比,只是比年輕代多了多線程垃圾回收而已
  • ParNew/CMS:當下比較高效的組合
  • Parallel Scavenge/Parallel Old:自動管理的組合
  • G1:最早進的收集器,可是須要JDK1.7update14以上

(2.1)Serial/Serial Old:

特色:

  • 年輕代Serial收集器採用單個GC線程實現"複製"算法(包括掃描、複製)
  • 年老代Serial Old收集器採用單個GC線程實現"標記-整理"算法
  • Serial與Serial Old都會暫停全部用戶線程(即STW)

說明:

STW(stop the world):編譯代碼時爲每個方法注入safepoint(方法中循環結束的點、方法執行結束的點),在暫停應用時,須要等待全部的用戶線程進入safepoint,以後暫停全部線程,而後進行垃圾回收。

適用場合:

  • CPU核數<2,物理內存<2G的機器(簡單來說,單CPU,新生代空間較小且對STW時間要求不高的狀況下使用)
  • -XX:UseSerialGC:強制使用該GC組合
  • -XX:PrintGCApplicationStoppedTime:查看STW時間
  • 因爲它實現相對簡單,沒有線程相關的額外開銷(主要指線程切換與同步),所以很是適合運行於客戶端PC的小型應用程序,或者桌面應用程序(好比swing編寫的用戶界面程序),以及咱們平時的開發、調試、測試等。

(2.2)ParNew/Serial Old:

說明:

  • ParNew除了採用多GC線程來實現複製算法之外,其餘都與Serial同樣,可是此組合中的Serial Old又是一個單GC線程,因此該組合是一個比較尷尬的組合,在單CPU狀況下沒有Serial/Serial Old速度快(由於ParNew多線程須要切換),在多CPU狀況下又沒有以後的三種組合快(由於Serial Old是單GC線程),因此使用其實很少。
  • -XX:ParallelGCThreads:指定ParNew GC線程的數量,默認與CPU核數相同,該參數在於CMS GC組合時,也可能會用到

(2.3)Parallel Scavenge/Parallel Old:

特色:

  • 年輕代Parallel Scavenge收集器採用多個GC線程實現"複製"算法(包括掃描、複製)
  • 年老代Parallel Old收集器採用多個GC線程實現"標記-整理"算法
  • Parallel Scavenge與Parallel Old都會暫停全部用戶線程(即STW)

說明:

  • 吞吐量:CPU運行代碼時間/(CPU運行代碼時間+GC時間)
  • CMS主要注重STW的縮短(該時間越短,用戶體驗越好,因此主要用於處理不少的交互任務的狀況)
  • Parallel Scavenge/Parallel Old主要注重吞吐量(吞吐量越大,說明CPU利用率越高,因此主要用於處理不少的CPU計算任務而用戶交互任務較少的狀況)

參數設置:

  • -XX:+UseParallelOldGC:使用該GC組合
  • -XX:GCTimeRatio:直接設置吞吐量大小,假設設爲19,則容許的最大GC時間佔總時間的1/(1 +19),默認值爲99,即1/(1+99)
  • -XX:MaxGCPauseMillis:最大GC停頓時間,該參數並不是越小越好
  • -XX:+UseAdaptiveSizePolicy:開啓該參數,-Xmn/-XX:SurvivorRatio/-XX:PretenureSizeThreshold這些參數就不起做用了,虛擬機會自動收集監控信息,動態調整這些參數以提供最合適的的停頓時間或者最大的吞吐量(GC自適應調節策略),而咱們須要設置的就是-Xmx,-XX:+UseParallelOldGC或-XX:GCTimeRatio兩個參數就好(固然-Xms也指定上與-Xmx相同就好)

適用場合:

  • 不少的CPU計算任務而用戶交互任務較少的狀況
  • 不想本身去過多的關注GC參數,想讓虛擬機本身進行調優工做
  • 對吞吐量要求較高,或須要達到必定的量。

(2.4)ParNew/CMS:

說明:

  • 以上只是年老代CMS收集的過程,年輕代ParNew看"2.二、ParNew/Serial Old"就好
  • CMS是多回收線程的,不要被上圖誤導,默認的線程數:(CPU數量+3)/4
  • CMS主要注重STW的縮短(該時間越短,用戶體驗越好,因此主要用於處理不少的交互任務的狀況)
    特色:

  • 年輕代ParNew收集器採用多個GC線程實現"複製"算法(包括掃描、複製)
  • 年老代CMS收集器採用多線程實現"標記-清除"算法
    • 初始標記:標記與根集合節點直接關聯的節點。時間很是短,須要STW
    • 併發標記:遍歷以前標記到的關聯節點,繼續向下標記全部存活節點。時間較長。
    • 從新標記:從新遍歷trace併發期間修改過的引用關係對象。時間介於初始標記與併發標記之間,一般不會很長。須要STW
    • 併發清理:直接清除非存活對象,清理以後,將該線程佔用的CPU切換給用戶線程
  • 初始標記與從新標記都會暫停全部用戶線程(即STW),可是時間較短;併發標記與併發清理時間較長,可是不須要STW

關於併發標記期間怎樣記錄發生變更的引用關係對象,在從新標記期間怎樣掃描這些對象

缺點:

  • 併發標記與併發清理:按照說明的第二點來說,假設有2個CPU,那麼其中有一個CPU會用於垃圾回收,而另外一個用於用戶線程,這樣的話,以前是兩CPU運行用戶線程,如今是一個,那麼效率就會急劇降低。也就是說,下降了吞吐量(即下降了CPU使用率)。
  • 併發清理:在這一過程當中,產生的垃圾沒法被清理(由於發生在從新標記以後)
  • 併發標記與併發清理:因爲是與用戶線程併發的,因此用戶線程可能會分配對象,這樣既可能對象直接進入年老代(例如,大對象),也可能進入年輕代後,年輕代發生minor GC,這樣的話,實際上要求咱們的年老代須要預留必定空間,也就是說要在年老代還有必定空間的狀況下就要進行垃圾回收,留出必定內存空間來供其餘線程使用,而不能等到年老代快爆滿了才進行垃圾回收,經過-XX:CMSInitiatingOccupancyFraction來指定當年老代空間滿了多少後進行垃圾回收
  • 標記-清理算法:會產生內存碎片,因爲是在老年代,可能會提早觸發Full GC(這正是咱們要儘可能減小的)
    參數設置:

  • -XX:+UseConcMarkSweepGC:使用該GC組合
  • -XX:CMSInitiatingOccupancyFraction:指定當年老代空間滿了多少後進行垃圾回收
  • -XX:+UseCMSCompactAtFullCollection:(默認是開啓的)在CMS收集器頂不住要進行FullGC時開啓內存碎片整理過程,該過程須要STW
  • -XX:CMSFullGCsBeforeCompaction:指定多少次FullGC後才進行整理
  • -XX:ParallelCMSThreads:指定CMS回收線程的數量,默認爲:(CPU數量+3)/4

適用場合:

  • 用於處理不少的交互任務的狀況
  • 方法區的回收通常使用CMS,配置兩個參數:-XX:+CMSPermGenSweepingEnabled與-XX:+CMSClassUnloadingEnabled
  • 適用於一些須要長期運行且對相應時間有必定要求的後臺程序

(2.5)G1

說明:

  • 從上圖來看,G1與CMS相比,僅在最後的"篩選回收"部分不一樣(CMS是併發清除),實際上G1回收器的整個堆內存的劃分都與其餘收集器不一樣。
  • CMS須要配合ParNew,G1可單獨回收整個空間

原理:

  • G1收集器將整個堆劃分爲多個大小相等的Region
  • G1跟蹤各個region裏面的垃圾堆積的價值(回收後所得到的空間大小以及回收所需時間長短的經驗值),在後臺維護一張優先列表,每次根據容許的收集時間,優先回收價值最大的region,這種思路:在指定的時間內,掃描部分最有價值的region(而不是掃描整個堆內存),並回收,作到儘量的在有限的時間內獲取儘量高的收集效率。

運做流程:

  • 初始標記:標記出全部與根節點直接關聯引用對象。須要STW
  • 併發標記:遍歷以前標記到的關聯節點,繼續向下標記全部存活節點。
    在此期間全部變化引用關係的對象,都會被記錄在Remember Set Logs中
  • 最終標記:標記在併發標記期間,新產生的垃圾。須要STW
  • 篩選回收:根據用戶指定的指望回收時間回收價值較大的對象(看"原理"第二條)。須要STW

優勢:

  • 停頓時間能夠預測:咱們指定時間,在指定時間內只回收部分價值最大的空間,而CMS須要掃描整個年老代,沒法預測停頓時間
  • 無內存碎片:垃圾回收後會整合空間,CMS採用"標記-清理"算法,存在內存碎片
  • 篩選回收階段:
    • 因爲只回收部分region,因此STW時間咱們可控,因此不須要與用戶線程併發爭搶CPU資源,而CMS併發清理須要佔據一部分的CPU,會下降吞吐量。
    • 因爲STW,因此不會產生"浮動垃圾"(即CMS在併發清理階段產生的沒法回收的垃圾)

適用範圍:

  • 追求STW短:若ParNew/CMS用的挺好,就用這個;若不符合,用G1
  • 追求吞吐量:用Parallel Scavenge/Parallel Old,而G1在吞吐量方面沒有優點
相關文章
相關標籤/搜索