天天看點

垃圾收集器與記憶體配置設定政策——深入了解Java虛拟機垃圾收集器與記憶體配置設定政策

垃圾收集器與記憶體配置設定政策

一、垃圾收集器回收區域

程式計數器、虛拟機棧、本地方法棧這3個區域随線程而生、随線程而滅。棧中棧幀随着方法的進入和退出有條不紊地執行入棧和出棧操作。每一個棧幀中配置設定多少記憶體在類結構确定時就已經知道(編譯器可知),于是這些區域的記憶體配置設定與回收具備确定性,在這幾個區域就不需要過多考慮回收的問題。
Java堆與方法區記憶體的配置設定和回收是動态的,垃圾收集器關注的是這部分記憶體。
           

二、對象存活判定算法

1.引用計數算法

給對象添加一個引用計數器,每當一個地方引用它時,計數器值加1;當引用失效時,計數器值減1.任何時刻計數器值為0的對象不可能再被使用。——這種算法判定效率高,大部分情況下是不錯的算法(微軟的

COM(Component Object Model)

技術、

Python

語言等),但缺點是這種算法很難解決對象之間互相循環引用的問題。

/**
 * testGC()方法執行後,objA和objB會不會被GC呢? 
 */
public class ReferenceCountingGC {
    public Object instance = null;
    private static final int _1MB =  * ;

    /**
     * 這個成員屬性的唯一意義就是占點記憶體,以便在能在GC日志中看清楚是否有回收過
     */
    private byte[] bigSize = new byte[ * _1MB];

    public static void testGC() {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB; //互相引用
        objB.instance = objA;

        objA = null; //變量objA、objB不指向這兩個對象了,且對象不會再被通路
        objB = null;

        // 假設在這行發生GC,objA和objB是否能被回收?
        System.gc(); //引用計數器法不會通知GC回收對象,可達性分析算法會通知GC回收對象。
    }
}
           

2.可達性分析(

Reachability Analysis

)算法

思路:通過一系列被稱為是

GC Roots

的對象作為起始點,從這些節點開始向下搜尋,搜尋走到過的路徑稱為引用鍊(

Reference Chain

),當一個對象到

GC Roots

沒有任何引用鍊相連(從

GC Roots

到這個對象不可達),就說這個對象不可用。

垃圾收集器與記憶體配置設定政策——深入了解Java虛拟機垃圾收集器與記憶體配置設定政策

其中對象5、6、7不可達,判定為可回收的對象。

在Java語言中,可做為

GC Roots

的對象有:(1) 虛拟機棧(棧幀中的本地變量表)中引用的對象; (2)本地方法棧中

JNI(Java Native Interface)

引用的對象; (3)方法區中常量引用的對象; (4)方法區中類靜态屬性引用的對象.

三 、引用說明

  1. 強引用(

    Object obj = new Object();

    )

    隻要強引用存在,垃圾收集器不會回收被引用的對象。生存時間:

    JVM

    停止運作時終止。
  2. 軟引用

    軟引用描述一些還有用但并非必須的對象。在系統将要發生記憶體溢出異常時,将會把這些對象列進回收對象範圍内,進行第二次回收(第一次回收無引用、弱引用對象)。如果這次回收還沒有足夠的記憶體,才會抛出

    OOM

    異常。生存時間:記憶體不足時終止。
  3. 弱引用

    描述非必需對象,強度比軟引用更弱一些,被弱引用關聯的對象隻能生存到下一次

    GC

    發生之前。當垃圾收集器

    GC

    時,無論目前記憶體是否足夠,都會回收掉被弱引用關聯的對象。 生存時間: 下一次

    GC

    之前。
  4. 虛引用

    一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,但也無法通過虛引用來取得一個對象執行個體。唯一目的: 能在這個對象被回收時,收到一個系統通知。

四、對象存活判定之後的事宜(對象自我拯救,兩次标記)

可達性算法判定對象是否存活之後,對象并非“非死不可”,對象真正死亡經曆兩個标記過程。當對象在進行可達性分析之後沒有

GC Roots

相連接配接的引用鍊,該對象會被第一次标記并進行篩選,判定是否覆寫了

finalize()

方法。如果對象沒有覆寫

finalize()

方法或者

finalize()

已經被虛拟機調用過(隻能調用一次), 則直接回收該對象。

如果對象覆寫了

finalize()

方法且沒有被調用過,則将其加入到

F-Queue

隊列之中,由虛拟機建立的一個低優先級

Finalizer

線程去執行調用隊列裡的這個方法。

finalize()

方法是對象逃脫死亡的最後一次機會,執行之後,

GC

将對

F-Queue

中的對象進行第二次小規模标記,如果對象重新 與引用鍊上的對象進行了關聯,則第二次标記時該對象被移除出”即将回收”的集合,否則該對象就被真正回收了。

結論: 1. 對象可以在

GC

時自我拯救(

finalize()方法

);2.

finalize()

方法最多被系統自動執行一次。

五、回收方法區

永久代回收主要是廢棄常量與無用的類。

1. 廢棄常量

判斷方法:可達性分析

2. 無用的類(

-Xnoclassgc

控制)

  1. 該類所有的執行個體都已經被回收,即

    Java

    堆中不存在該類的任何執行個體。
  2. 加載該類的

    ClassLoader

    已經被回收
  3. 該類對應的

    java.lang.Class

    對象沒有在任何地方被引用,無法在任何地方通過反射通路該類的對象。

六、垃圾收集算法

1. 标記-清除算法

垃圾收集器與記憶體配置設定政策——深入了解Java虛拟機垃圾收集器與記憶體配置設定政策

首先标記出所有需要回收的對象,在标記完成後統一回收所有被标記的對象。标記過程與對象自我拯救中标記過程一樣。

缺點:

(1) 效率問題

标記和清除涉及兩次堆的掃描,耗時嚴重。

(2) 空間問題

标記清楚之後會産生大量不連續的記憶體碎片,空間碎片過多會導緻以後在程式運作過程中需要配置設定大對象時,無法找到足夠的連續記憶體(

Free List(空閑清單)

)而必須觸發一次

GC

動作。

2. 複制算法

垃圾收集器與記憶體配置設定政策——深入了解Java虛拟機垃圾收集器與記憶體配置設定政策

原理:

将可用記憶體按照容量分為大小相等的兩塊,每次隻使用其中的一塊。當這一塊記憶體用完了,就将還存活的對象複制到另外一塊上去,然後把已使用過的記憶體空間一次清理掉。這樣子,每次都是對整個半區進行記憶體回收,記憶體配置設定時不用考慮記憶體碎片的情況,隻要移動堆頂指針按順序配置設定記憶體即可,比較簡單高效。

優點:

1. 清除過程效率高

2. 無記憶體碎片,利用

bump the pointer(指針碰撞)

實作記憶體快速配置設定。

缺點:

需要雙倍空間,适用于存活率比較低的對象(複制對象不多)。

現代虛拟機實作

1.将新生代分為記憶體較大的

Eden

區域和兩塊記憶體較小的

Survivor

區域(8:1:1),每次使用

Eden

和其中的一塊

Survivor

區域。當回收時,将

Eden

Survivor

中還存活的對象複制到另外一塊

Survivor

中,最後清理掉

Eden

與剛才用過的

Survivor

區域。

2.這個實作需要老年代進行記憶體配置設定擔保,如果另外一塊

Survivor

空間沒有足夠空間存放上一次新生代收集下來存活的對象,這些對象直接通過配置設定擔保機制進入老年代。

3.标記-整理算法

垃圾收集器與記憶體配置設定政策——深入了解Java虛拟機垃圾收集器與記憶體配置設定政策

過程:标記過程與”标記-清除”算法一緻,但不是直接對可回收對象進行清理,而是讓存活的對象都想一端移動,然後直接清理掉端邊界之外的記憶體。

優點:

無記憶體碎片,可利用

bum the pointer(指針碰撞)

進行快速記憶體配置設定

缺點:

需要移動對象的成本,适用于老年代

4.分代收集算法

根據對象存活周期不同,将記憶體劃分為幾塊,即新生代與老年代。新生代一般采用複制算法收集垃圾,老年代采用”标記-整理“算法回收垃圾。

七、垃圾收集器

垃圾收集器與記憶體配置設定政策——深入了解Java虛拟機垃圾收集器與記憶體配置設定政策

并行與并發的差別

1. 并行: 多條垃圾收集線程并行工作,使用者線程任然處于等待狀态。

2. 并發:使用者線程與垃圾收集線程同時運作。

Young Generation:

  • Serial
  • ParNew
  • Parallel Scavenge
  • G1

Tenured Generation:

  • CMS
  • Serial Old(MSC)
  • Parallel Old
  • G1

1.

Serial GC

  1. 特性

    單線程收集器,隻會使用一個

    CPU

    或者一條線程去完成垃圾收集工作,同時在它進行垃圾回收時,必須暫停其他的工作線程,知道垃圾收集結束。(

    Stop the World

    )
  2. 工作原理
    垃圾收集器與記憶體配置設定政策——深入了解Java虛拟機垃圾收集器與記憶體配置設定政策

    Serial

    新生代采用複制算法,暫停所有使用者線程,

    Serial Old

    采用标記-整理算法,暫停所有線程。
  3. 使用場景

    虛拟機運作在

    Client

    模式下預設的新生代收集器,簡單而高效。

2.

ParNew GC

  1. 特性

    使用多線程進行垃圾回收。

  2. 運作原理
    垃圾收集器與記憶體配置設定政策——深入了解Java虛拟機垃圾收集器與記憶體配置設定政策

    ParNew

    新時代采用複制算法。
  3. 使用場景

    Server

    模式下首選的新生代收集器。

    ParNew GC

    使用

    -XX:+UseConcMarkSweepGC

    選項後(ParNew + CMS);

    -XX:+UseParNewGC

    (ParNew + Serial Old)

-XX:ParallelGCThread

限制垃圾回收線程數。

3.

Parallel Scavenge

收集器(吞吐量優先)

  1. 特性

    新生代收集器,複制算法,并行多線程收集器。

    CMS

    等收集器關注的是盡可能縮短垃圾收集的使用者線程停頓時間,而

    Parallel Scavenge

    收集器為了達到一個可控制的吞吐量。
    1.吞吐量是

    CPU

    用于運作使用者代碼的時間與

    CPU

    總消耗時間的比值(

    Throughput

    ).

    2.停頓時間越短,越适合于與使用者互動的程式,響應速度快。高吞吐量則可以高效利用

    CPU

    時間,經快完成程式的運算任務,适用于背景運算,不需要較多的互動任務。
  2. 參數控制

    -XX:MaxGCPauseMillis

    :控制最大垃圾收集停頓時間

    -XXX:GCTimeRatio

    : 直接設定吞吐量大小

    -XX:+UseAdaptiveSizePolicy

    打開後,虛拟機會根據目前系統運作情況收集性能監控資訊,動态調整

    -XX:SurvivorRario

    等參數以提供最合适的停頓時間或者最大吞吐量。(自适應調節政策)

4.

Serial Old

  1. 特性

    單線程,标記-整理算法。

  2. 原理
    垃圾收集器與記憶體配置設定政策——深入了解Java虛拟機垃圾收集器與記憶體配置設定政策
  3. 使用場景

    (1)

    Client

    模式下的虛拟機使用。

    (2) 如果在

    Server

    模式下,那麼它主要還有兩大用途:一種用途是在JDK 1.5以及之前的版本中與

    Parallel Scavenge

    收集器搭配使用,另一種用途就是作為

    CMS

    收集器的後備預案,在并發收集發生

    Concurrent Mode Failure

    時使用。

5.

Parallel Old GC

  1. 特性

    Parallel Old

    Parallel Scavenge

    收集器的老年代版本,使用多線程和“标記-整理”算法。
  2. 原理
    垃圾收集器與記憶體配置設定政策——深入了解Java虛拟機垃圾收集器與記憶體配置設定政策
  3. 使用場景

    在注重吞吐量以及

    CPU

    資源敏感的場合,都可以優先考慮

    Parallel Scavenge

    Parallel Old

    收集器。
6.

CMS GC

  1. 特性

    CMS(Concurrent Mark Sweep)

    收集器是一種以擷取最短回收停頓時間為目标的收集器。目前很大一部分的Java應用集中在網際網路站或者B/S系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給使用者帶來較好的體驗。CMS收集器就非常符合這類應用的需求。
  2. 運作原理
    垃圾收集器與記憶體配置設定政策——深入了解Java虛拟機垃圾收集器與記憶體配置設定政策

CMS

收集器是基于“标記—清除”算法實作的,它的運作過程如下:

  • 初始标記

    初始标記隻是标記一下

    GC Roots

    能直接關聯到的對象,速度很快,需要“

    Stop The World

    ”.
  • 并發标記

    并發标記階段就是進行

    GC Roots Tracing

    的過程,周遊其他的對象進行可達性分析。
  • 重新标記

    重新标記階段是為了修正并發标記期間因使用者程式繼續運作而導緻标記産生變動的那一部分對象的标記記錄,這個階段的停頓時間一般會比初始标記階段稍長一些,但遠比并發标記的時間短,仍然需要“

    Stop The World

    ”。
  • 并發清除

    并發清除階段清除對象。

3.優點與缺點

優點: 并發收集、低停頓。

缺點:

1. CMS收集器對CPU資源非常敏感;

2. CMS收集器無法處理浮動垃圾(CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure”失敗而導緻另一次Full GC的産生);

3. CMS收集器會産生大量空間碎片.

7.

G1

收集器

  1. 特性

    G1(Garbage-First)

    是一款面向服務端應用的垃圾收集器。
    • 并行并發
    • 分代收集
    • 空間整合
    • 可預測的停頓
  2. 運作原理

    在G1之前的其他收集器進行收集的範圍都是整個新生代或者老年代,而G1不再是這樣。使用G1收集器時,Java堆的記憶體布局就與其他收集器有很大差别,它将整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是實體隔離的了,它們都是一部分Region(不需要連續)的集合。

    G1收集器之是以能建立可預測的停頓時間模型,是因為它可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裡面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在背景維護一個優先清單,每次根據允許的收集時間,優先回收價值最大的Region(這也就是Garbage-First名稱的來由)。這種使用Region劃分記憶體空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間内可以擷取盡可能高的收集效率。

  3. 執行過程
    垃圾收集器與記憶體配置設定政策——深入了解Java虛拟機垃圾收集器與記憶體配置設定政策
    1. 初始标記(Initial Marking)

      初始标記階段僅僅隻是标記一下GC Roots能直接關聯到的對象,并且修改TAMS(Next Top at Mark Start)的值,讓下一階段使用者程式并發運作時,能在正确可用的Region中建立新對象,這階段需要停頓線程,但耗時很短。

    2. 并發标記(Concurrent Marking)

      并發标記階段是從GC Root開始對堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與使用者程式并發執行。

    3. 最終标記(Final Marking)

      最終标記階段是為了修正在并發标記期間因使用者程式繼續運作而導緻标記産生變動的那一部分标記記錄,虛拟機将這段時間對象變化記錄線上程Remembered Set Logs裡面,最終标記階段需要把Remembered Set Logs的資料合并到Remembered Set中,這階段需要停頓線程,但是可并行執行。

    4. 篩選回收(Live Data Counting and Evacuation)

      篩選回收階段首先對各個Region的回收價值和成本進行排序,根據使用者所期望的GC停頓時間來制定回收計劃,這個階段其實也可以做到與使用者程式一起并發執行,但是因為隻回收一部分Region,時間是使用者可控制的,而且停頓使用者線程将大幅提高收集效率。

8.

GC

總結

垃圾收集器與記憶體配置設定政策——深入了解Java虛拟機垃圾收集器與記憶體配置設定政策
垃圾收集器與記憶體配置設定政策——深入了解Java虛拟機垃圾收集器與記憶體配置設定政策

八、記憶體配置設定與回收政策

1.對象首先在

Eden

區域配置設定

大多數情況下,對象在新生代Eden區中配置設定。當Eden區沒有足夠記憶體的時候,虛拟機發生一次Minor GC(新生代).
           

2. 大對象直接進入老年代

大對象指的是需要大量連續記憶體空間的對象。
-XX:PretenureSizeThreshold  使得大于設定值的對象直接進入老年代。
           

3.長期存活的對象進入老年代

虛拟機給每個對象定義了一個對象年齡(Age)計數器。如果對象在Eden出生并經過第一次Minor GC後仍然存活,并且能被Survivor容納的話,将被移動到Survivor空間中,并且對象年齡設為1。對象在Survivor區中每“熬過”一次Minor GC,年齡就增加1歲,當它的年齡增加到一定程度(預設為15歲),就将會被晉升到老年代中。對象晉升老年代的年齡門檻值,可以通過參數-XX:MaxTenuringThreshold設定。
           

4. 動态對象年齡判定

為了能更好地适應不同程式的記憶體狀況,虛拟機并不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。
           

5. 空間配置設定擔保

在發生Minor GC之前,虛拟機會先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間,如果這個條件成立,那麼Minor GC可以確定是安全的。如果不成立,則虛拟機會檢視HandlePromotionFailure設定值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大于曆次晉升到老年代對象的平均大小,如果大于,将嘗試着進行一次Minor GC,盡管這次Minor GC是有風險的;如果小于,或者HandlePromotionFailure設定不允許冒險,那這時也要改為進行一次Full GC。
前面提到過,新生代使用複制收集算法,但為了記憶體使用率,隻使用其中一個Survivor空間來作為輪換備份,是以當出現大量對象在Minor GC後仍然存活的情況(最極端的情況就是記憶體回收後新生代中所有對象都存活),就需要老年代進行配置設定擔保,把Survivor無法容納的對象直接進入老年代。與生活中的貸款擔保類似,老年代要進行這樣的擔保,前提是老年代本身還有容納這些對象的剩餘空間,一共有多少對象會活下來在實際完成記憶體回收之前是無法明确知道的,是以隻好取之前每一次回收晉升到老年代對象容量的平均大小值作為經驗值,與老年代的剩餘空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間。
取平均值進行比較其實仍然是一種動态機率的手段,也就是說,如果某次Minor GC存活後的對象突增,遠遠高于平均值的話,依然會導緻擔保失敗(Handle Promotion Failure)。如果出現了HandlePromotionFailure失敗,那就隻好在失敗後重新發起一次Full GC。雖然擔保失敗時繞的圈子是最大的,但大部分情況下都還是會将HandlePromotionFailure開關打開,避免Full GC過于頻繁,參見代碼清單3-9,請讀者在JDK 6 Update 24之前的版本中運作測試。
           

繼續閱讀