JVM系列-02-GC-掃盲

[TOC]java

聲明

本篇文章是本人閱讀《深刻理解JVM》和《java虛擬機規範》時的筆記。
記錄的都是一些概念性的東西。
JVM是HotSpot,jdk1.7。
大神繞路,不喜勿噴。程序員

1 GC算法

先來蜻蜓點水般地瀏覽一些著名的GC算法。
這裏也僅僅是說一下大體過程,具體細節的介紹對於我一個Java程序員來講表示無能爲力,由於底層實現要牽扯到具體的實現語言了,並且不一樣的JVM實現商確定有不一樣的實現細節。算法

1.1 標記/清除算法

這種算法的大概過程是:多線程

  • 標記出全部須要回收的對象併發

  • 統一回收全部被標記的對象佈局

這種算法很直觀,但他的缺點以下:性能

  • 標記和清除的兩個階段,效率並非很好,由於回收的粒度太細了spa

  • 清除後的內存區域通常都是千瘡百孔,可用內存區域通常都不連續線程

1.2 複製算法

上面說的標記/清除算法不太好的主要緣由就是其回收粒度太過細微了。
籤於此,複製算法的主要作法是:日誌

  • 將內存分爲大小相等的兩塊,暫且稱之爲內存塊

  • 每次當某一內存塊(A)佔滿以後,將該內存塊(A)的有用數據複製到另一塊內存塊(B)

  • 將A內存塊的整個塊直接清除

這種算法相比於標記/清除算法的最大特色是:

  • 每次回收都是之內存塊爲單位(粒度較大)

  • 只有兩個內存塊,收集完後內存不連續的狀況也就不用考慮了

  • 可是,將整個內存分爲兩塊,實際的可用內存也就減半了(這就有點沒法接受了)

  • 存在大量的對象複製操做

1.3 標記/整理算法

上面說的複製算法的最大缺點就是對象的複製操做。尤爲是在有效的對象不少的狀況下。

這裏的標記/整理算法的大體過程是:

  • 標記應該回收的區域

  • 將有用的對象/數據集中移動到一塊區域,暫且稱之爲"有效區",有效區的位置每每是在兩端的某一端

  • 將"有效區"以外的區域集體清理

1.4 分代收集算法(Generational Collection)

既然上面說集中算法都各有優劣,那麼根據他們各自的優勢,在不一樣的狀況下使用最優的算法會不會更好呢?

分代收集的大體思路就是這樣的:

  • 將JVM堆內存分爲新生代和年老代

  • 年老代中的對象存活率通常都很高,採用'標記/清理'或'標記/複製'算法

  • 新生代中通常對象的'死亡率'都很高,採用複製算法

2 GC的代價——Stop The World

上面說了一大堆GC的理論。可是忽略了一點:

怎麼肯定哪些對象或內存區域是能夠被回收的呢???

在java中對於對象是否還「活着」,採用的不是像Python或者其餘語言中的"引用計數"的方法。
java中採用的是"可達性分析"。
至於可達性分析的細節不必去深究,可是由"判斷對象是否還存活?"引出的另外一個問題卻不得不考慮,看下文。

不管採用什麼方法去區分哪些對象還活着,不得不作的一個讓步就是:這個斷定過程當中必須暫時讓其餘全部的線程都暫時停頓,這個現象對於JVM中的各個對象來講就至關於整個世界中止了。也就是所謂的Stop The World

這個停頓固然是有必要的,好比你開始分析對象的存活狀態時一個對象是無用的,當你分析完成後那個對象卻讓其餘線程操做了變成有效對象了。

因此,在整個判斷過程當中,要可以確保一致性。也就免不了Stop The World

固然,應用的規模越大,Stop The World帶來的影響越大。
因此,頻繁的GC也不見得是好事。

3 垃圾收集器

上面說的都是GC的大體理論知識,如今看看GC的實現:垃圾收集器。

3.1 Serial收集器

Serial收集器是衆多垃圾收集器中的元老。是一個單線程的收集器。在它進行垃圾收集時,必須暫停其餘全部的工做線程,直到它收集結束(Stop The World)。
雖然它的出現很是早,可是它依然是虛擬機運行在Client模式下的默認新生代收集器,也有其獨特的優勢:

  • 簡單而高效(與其餘收集器的單線程比)

  • 對於限定單個CPU的環境來講,Serial收集器因爲沒有線程交互的開銷

3.2 ParNew收集器

這個ParNew的介紹是來自《深刻理解JVM》的做者說的,與本人沒任何關係 ^_^ .. ^_^

ParNew收集器其實就是Serial收集器的多線程版本,除了使用多條線程進行垃圾收集以外,其他行爲包括Serial收集器可用的全部控制參數(例如:-XX:SurvivorRatio、 -XX:PretenureSizeThreshold、 -XX:HandlePromotionFailure等)、 收集算法、 Stop The World、 對象分配規則、 回收策略等都與Serial收集器徹底同樣,在實現上,這兩種收集器也共用了至關多的代碼。

  • 是許多運行在Server模式下的虛擬機中首選的新生代收集器,其中有一個與性能無關但很重要的緣由是,除了Serial收集器外,目前只有它能與CMS收集器配合工做

  • ParNew收集器在單CPU的環境中絕對不會有比Serial收集器更好的效果

    • 甚至因爲存在線程交互的開銷,該收集器在經過超線程技術實現的兩個CPU的環境中都不能百分之百地保證能夠超越Serial收集器。

    • 固然,隨着可使用的CPU的數量的增長,它對於GC時系統資源的有效利用仍是頗有好處的。 它默認開啓的收集線程數與CPU的數量相同,在CPU很是多的環境下,可使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數

3.3 Parallel Scavenge收集器

3.3.1 簡介

他的特色以下:

  • 是一個新生代收集器

  • 使用複製算法的收集器

  • 並行的多線程收集器

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

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

    • 也常常稱爲「吞吐量優先」收集器

3.3.2 參數

  • -XX:MaxGCPauseMillis ==> 控制最大垃圾收集停頓時間,大於零的毫秒數

  • -XX:GCTimeRatio ==> 直接設置吞吐量大小,吞吐量的倒數,既然是個比率,也就是個0到100的整數

  • -XX:+UseAdaptiveSizePolicy

    • 是一個開關參數,當這個參數打開以後,就不須要手工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、 晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數了.

    • 虛擬機會根據當前系統的運行狀況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱爲GC自適應的調節策略(GC Ergonomics)

3.4 Serial Old收集器

  • Serial收集器的老年代版本

  • 一個單線程收集器

  • 使用「標記-整理」算法

3.5 Parallel Old收集器

  • 是Parallel Scavenge收集器的老年代版本

  • 使用多線程和「標記-整理」算法

3.6 CMS收集器

  • CMS:Concurrent Mark Sweep

  • 是一種以獲取最短回收停頓時間爲目標的收集器

    • 此處的停頓指的就是上文提到的"Stop The World"

  • 其名稱中的MS指的就是Mark Sweep,它採用的算法就是標記/清除

  • 他有另一個名字就是:Concurrent Low Pause Collector(併發、低停頓)

他的缺點以下:

  • 對CPU資源很是敏感

    • 它雖然不會致使用戶線程停頓,可是會由於佔用了一部分線程而致使應用程序變慢,總吞吐量會下降

  • 沒法處理浮動垃圾

  • 標記/清除------->內存不連續

3.7 G1收集器

  • 是一款面向服務端應用的垃圾收集器

  • 並行與併發

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

  • 分代收集

  • 空間整合

    • 不會產生內存空間碎片,收集後能提供規整的可用內存

    • 分配大對象時不會由於沒法找到連續內存空間而提早觸發下次GC

  • 可預測的停頓:低停頓

《深刻理解JVM》一書是這麼說的:

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

4 GC日誌

GC日誌的格式乍看起來亂七八糟,烏漆嘛黑的。固然他確定是有格式的。就拿《深刻理解JVM》中的這段代碼來講吧:

public class ReferenceCountingGC {
    public Object instance = null;
    private static final int _1MB = 1024 * 1024;
    /**
     * 這個成員屬性的惟一意義就是佔點內存,以便能在GC日誌中看清楚是否被回收過
     */
    byte[] bigSize = new byte[2 * _1MB];

    public static void main(String[] args) {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
        // 假設在這行發生GC,objA和objB是否能被回收?
        System.gc();
    }
}

虛擬機參數:

-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps

在個人機器(jdk1.7)上輸出以下:

2016-12-17T16:11:19.650+0800: 0.093: [GC [PSYoungGen: 5427K->568K(38400K)] 5427K->568K(124416K), 0.0016819 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
2016-12-17T16:11:19.652+0800: 0.095: [Full GC [PSYoungGen: 568K->0K(38400K)] [ParOldGen: 0K->463K(86016K)] 568K->463K(124416K) [PSPermGen: 2514K->2513K(21504K)], 0.0109008 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 38400K, used 998K [0x00000007d5c80000, 0x00000007d8700000, 0x0000000800000000)
  eden space 33280K, 3% used [0x00000007d5c80000,0x00000007d5d79a60,0x00000007d7d00000)
  from space 5120K, 0% used [0x00000007d7d00000,0x00000007d7d00000,0x00000007d8200000)
  to   space 5120K, 0% used [0x00000007d8200000,0x00000007d8200000,0x00000007d8700000)
 ParOldGen       total 86016K, used 463K [0x0000000781600000, 0x0000000786a00000, 0x00000007d5c80000)
  object space 86016K, 0% used [0x0000000781600000,0x0000000781673eb0,0x0000000786a00000)
 PSPermGen       total 21504K, used 2520K [0x000000077c400000, 0x000000077d900000, 0x0000000781600000)
  object space 21504K, 11% used [0x000000077c400000,0x000000077c676178,0x000000077d900000)

解釋以下:

2016-12-17T16:11:19.650+0800
    -XX:+PrintGCDateStamps的做用,就是GC的時間了
0.093:表示的從JVM啓動以來通過的秒數
GC [PSYoungGen:....
    GC發生的區域
        PSYoungGen表示採用的收集器爲Parallel Scavenge
        若是使用的是Serial收集器,新生代名爲「Default New Generation」,顯示就是「[DefNew」
        若是使用的是ParNew收集器,新生代名稱爲「[ParNew」,意爲「Parallel New Generation」
        若是採用的是Parallel Scavenge收集器,新生代名稱就是「PSYoungGen」
「Full」,說明此次GC是發生了Stop-The-World

GC日誌,暫時就先寫這麼多吧,在後續的文章中再詳細介紹GC日誌。

5 和GC相關的JVM參數

注:如下參數總結來自《深刻理解JVM》一書

  • UseSerialGC : 是否使用Serial收集器

    • 啓用後將使用Serial + Serial Old的組合來進行垃圾回收

    • 這也是Client模式下的默認值

  • UseParNewGC : 是否使用ParNew收集器

    • 將使用ParNew + Serial Old的組合來進行垃圾回收

  • UseConcMarkSweepGC

    • 啓用後將使用ParNew + CMS + Serial Old的組合來進行垃圾回收

    • Serial Old 做爲CMS的後備收集器(Concurrent Mode Failure)

  • UseParallelGC

    • 使用Parallel Scavenge + Serial Old的組合來進行垃圾回收

    • 這也是Server模式下的默認值

  • UseParallelOldGC

    • 使用 Parallel Scavenge + Parallel Old的組合來進行垃圾回收

  • SurvivorRatio

    • 新生代中Eden和Survivor的比值

    • 默認爲8,即:Eden:Survivor=8:1

  • PretenureSizeThreshold

    • 這個大小值,表示對象大小大於多少以後直接分配到老年代而不進入新生代

  • MaxTenuringThreshold

    • 這個年齡值表示對象在通過多少次Minor GC以後就進入老年代

    • 每次Minor GC以後,對象的該屬性值就加1

  • UseAdaptiveSizePolicy

    • 動態調節堆中各個區域的大小和進入老年代的年齡

  • HandlePromotionFailure

    • 是否容許分配擔保失敗

    • 擔保失敗指的是: 老年代的剩餘空間大小沒法容納新生代中的Eden和Survivor的狀況

  • ParallelGCThreads

    • 並行GC的線程數

  • GCTimeRatio

    • GC時間佔總時間的比例

    • 只有在使用Parallel Scavenge的狀況下生效

    • 默認值:99

  • MaxGCPauseMillis

    • GC的最大停頓時間

    • 只有在使用Parallel Scavenge的狀況下生效

  • CMSInitiatingOccupancyFraction

    • CMS收集器在老年代空間被佔用多少後觸發GC

    • 只對CMS收集器生效

    • 默認值:68%

  • UseCMSCompactAtFullCollection

    • CMS收集器在完成垃圾回收後是否進行內存碎片整理

    • 只對CMS收集器生效

  • CMSFullGCsBeforeCompaction

    • CMS通過多少次GC後再進行碎片整理

    • 也就是設置CMS收集器在進行N次垃圾收集後再進行一次碎片整理

    • 只對CMS收集器生效

6 Minor GC 和 Full GC/Major GC

  • Minor GC指的是新生代的GC

    • Minor GC比較頻繁

    • 速度也比較快

  • Full GC/Major GC指的是老年代的GC

    • Full GC的速度通常比Minor GC慢10倍左右

    • Full GC的出現每每會有Minor GC的伴隨

參考文章

  • 《深刻理解JVM》

  • 《Java虛擬機規範》-JDK1.7

相關文章
相關標籤/搜索