《深入理解Java虛擬機》之垃圾收集器與內存分配策略

閱讀《深入理解Java虛擬機》第2版,結合JDK8的讀書筆記。當前文章爲書本的第3章節。

3.1.概述

GC需要完成3件事情:

  • 哪些內存需要回收?
  • 什麼時候回收?
  • 如何回收?

垃圾收集器關注的是Java堆的內存回收,因爲堆存放的是對象的實例,例如一個接口中不同的實現類所需要的內存可能是不一樣的,一個方法中不同的分支所需要的內存可能也不一樣,只有在程序處於運行期間才能知道會創建哪些對象,這部分內存的分配和回收都是動態的。

3.2.對象已死嗎

垃圾收集器在對堆進行回收前,第一件事就是要判斷這些對象是否存活?下文介紹判斷對象是否可以回收的方案:

  • 引用計數算法(Reference Counting)
  • 可達性分析算法(Reachability Analysis)

3.2.1.引用計數算法

每當有對象引用它時,計數器值加1,引用失效時,計數器值減1,任何時候計數器爲0的對象就是不可能再被使用。

算法實現簡單,判定效率也很高,在大部分情況下是一個不錯的算法。不過無法解決互相引用的問題。

public class ReferenceCounting{

    public Object instance = null;
    
    public static void main(String[] args){
    
        // 互相引用案例
        ReferenceCounting objA = new ReferenceCounting();
        ReferenceCounting objB = new ReferenceCounting();
        
        objA.instance = objB;
        objB.instance = objA;
    }
}

3.2.2.可達性分析算法

通過一系列的稱爲“GC Roots”的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference-Chain),當一個對象到GC Roots沒有任何引用鏈相連時(用圖論的話來說,就是從GC-Roots到該對象不可達),證明此對象不可用。

在JAVA中,可以作爲GC Roots的對象包括:

  1. 虛擬機棧(棧幀中的本地變量表)中的引用對象;
  2. 方法區中類靜態屬性引用的對象;
  3. 方法區中常量引用的對象;
  4. 本地方法棧中JNI(即一般說的Native方法)引用的對象。

3.2.3.引用等級

無論是通過引用計數算法還是可達性分析算法,判斷對象是否存活都和“引用”有關,在JDK1.2之後,Java對引用的概念進行了擴充,將應用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)四種。

  • 強引用

強引用就是指在程序代碼址中普遍存在的,類似"Object obj = new Object()"這裏的引用,只要強引用還存在,垃圾回收器就不會回收掉被引用的對象。

  • 軟引用

對於軟引用關聯的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的內存,纔會拋出內存溢出異常。

在JDK1.2之後,提供了SoftReference類來實現軟引用。

軟引用使用設計對象的cache。對於緩存,我們希望被緩存的對象最好常駐內存,但是如果內存喫緊,爲了防治發生內存溢出導致系統崩潰,可以允許虛擬機收回內存。

public static void main(String[] args) {
    // 模擬對象緩存
    Map<String, String>  map = new HashMap<>(3);
    map.put("a", "a-value");
    map.put("b", "b-value");
    map.put("c", "c-value");

    SoftReference<Map<String, String>> softReference = new SoftReference<Map<String, String>>(map);
    Map<String, String> cacheMap = softReference.get();

    System.out.println("a = " +cacheMap.get("a"));
}
  • 弱引用

對於弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉弱引用關聯的對象。

在JDK1.2之後,提供了WeakReference類來實現弱引用。用法同軟引用一樣。

通常用於Debug、內存監視工具等程序中。因爲這類程序一般要求即要觀察到對象,又不能影響該對象正常的GC過程。

  • 虛引用

一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。

在JDK1.2之後,提供了PhantomReference類來實現虛引用。用法同軟引用一樣。

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

3.2.4.生存還是死亡

對象被回收要經歷兩次標記過程:

  1. 對象在進行可達性分析後發現不可達,則被第一次標記並進行篩選,篩選的條件是該對象是否有必要執行finalize()方法。

當對象沒有重寫finalize方法或者該方法已經被調用過,這兩種情況都視爲“沒有必要執行”。

  1. 當發現對象有必要執行finalize()方法時,將其加入F-Queue對象。Finalizer線程(低優先級)執行F-Queue隊列,會執行該隊列中對象的finalize()方法,如果該對象還是不可達狀態,則被回收。

這裏的執行是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束。有可能對象的finalize()方法執行緩慢,也有可能finallize()方法是個死循環。

以下代碼演示對象的自我解救:

/**
 * @author guoyu.huang
 * @version 1.0.0
 */
public class FinalizeEscapeGC {

    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive() {
        System.out.println("i am still alive.");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("do finalize.");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC();
        // 等待被回收
        SAVE_HOOK = null;
        System.gc();

        Thread.sleep(500);

        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("i am dead.");
        }

        // 第二次等待被回收
        SAVE_HOOK = null;
        System.gc();

        Thread.sleep(500);

        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("i am dead.");
        }
    }
}


// 日誌打印:

do finalize.
i am still alive.
i am dead.

3.2.5.回收方法區

java虛擬機允許方法區不進行垃圾回收。方法區的垃圾回收主要分兩部分:廢棄常量和無用的類。

  • 廢棄常量

判斷常量是否廢棄,只要判斷該常量是否被引用即可。

例如:一個字符串“abc”已經進入常量池,但是當前系統沒有一個String對象引用常量池中的“abc”常量

  • 無用的類

判斷類是否無用,需要滿足以下三個條件:

  1. 該類所有實例都已經被回收
  2. 該類的ClassLoader已經被回收
  3. 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類。

虛擬機可以對滿足以上三個條件的無用類進行回收,並不是一定會回收。

是否對類進行回收,HotSpot虛擬機提供了-Xnoclassgc參數進行控制。

在大量使用反射,動態代理,CGLib等ByteCode框架,動態生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證方法區不會溢出。

3.3.垃圾收集算法

3.3.1.標記-清除算法

標記-清除算法(Mark-Sweep)是最基礎的收集算法。

該算法分爲標記和清除兩個階段,首先標記要被回收的對象,在標記完成之後統一回收所有被標記的對象。

該算法的不足:

  1. 標記和清除的效率都不高

第一階段標記,需要遍歷所有的對象,對要刪除的對象進行標記。第二階段刪除,遍歷所有的對象,對被標記的對象進行清除操作。當對象的數量很大的時候,這種方法的效率就很差了。

  1. 會產生過多不連續的空間碎片,導致後續無法申請比較大的連續內存

3.3.2.複製算法

複製算法(Copying)將內存平均分成兩塊,每次只使用其中一塊,當這一塊內存使用完之後,將還存活的對象複製到另一塊內存,然後將使用過的那塊內存全部清除。

因爲每次都是對整個半區進行內存回收,內存分配的時候不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。

該算法的不足:造成內存損失

3.3.3.標記-整理算法

標記-整理(Mark-Compact)算法,標記過程仍然與標記-清除算法的一樣,但後續步驟不是直接對可回收對象進行清理,而是將存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。

3.3.4.分代收集算法

根據對象存活週期的不同將內存分爲新生代和老年代。

  • 新生代

對象存活率不高。一般選擇複製算法,要操作的對象少,效率高,還有老年代可以進行內存的分配擔保,可以減少內存損失。

  • 老年代

對象存活率高。而且沒有額外空間對其進行分配擔保,所以就必須使用“標記-清理”或者“標記-整理”算法來進行回收。

  • JDK8 HotSpot 新生代收集算法

將新生代內存分爲三塊,分別爲Eden和兩塊Survivor(from和to),默認比例爲8:1:1,當回收的時候將Eden和其中一塊Survivor中還存活的對象一次性複製到另一塊Survivor內存中,如果這塊內存不夠存放,需要其他內存(這裏指老年代)進行分配擔保。

3.4.HotSpot的算法實現

3.4.1.枚舉根節點

使用可達性分析算法,判斷對象是否存活。

GC Roots的節點主要在全局性的引用(例如常量或類靜態屬性)與執行上下文(例如棧幀中的本地變量表)中,現在很多應用僅僅方法區就有數百兆,如果逐個檢查這裏面的引用,必然會消耗很多時間。因此引入了準確式GC。

  • 準確式GC

準確式GC是指虛擬機可以知道內存中某個位置的數據具體是什麼類型。

目前主流的Java虛擬機使用的都是準確式GC,所以當執行系統停頓下來後,虛擬機應該是有辦法得知哪些地方存放着對象引用。

在HotSpot的實現中,是使用一組稱爲OopMap的數據結構來達到這個目的的,在類加載完成的時候,HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和寄存器中哪些位置是引用。這個,GC在掃描時就可以直接得知這些信息了。

另外,可達性分析對執行時間的敏感還體現在GC停頓上,因爲分析工作必須在一個能確保一致性的快照中進行。這個原因導致GC進行時必須停頓所有的java執行線程(Sun將這件事情稱爲“Stop The World”)。

3.4.2.安全點

在OopMap的協助下,HotSpot可以快速且準確地完成GC Roots枚舉。出於性能考慮,HotSpot不能爲每條指令都生成OopMap,前面也提到只是在“特定的位置”記錄了這些信息,這些特定位置稱爲安全點(safepoint)。

安全點太少,需要長時間才能進入安全點導致GC等待時間太長;大多,會導致頻繁進入GC增大運行時的負荷。所以,安全點的選定基本上是以程序“是否具有讓程序長時間執行的特徵”爲標準進行選定的。

“長時間執行”的最明顯特徵就是指令複用,例如方法調用(方法返回前),循環跳轉(循環的末尾),異常跳轉(拋出異常的位置)等。

接下來就是要考慮如何在GC發生時,讓所有線程跑到安全點上再停頓下來。兩種方案:搶先式中斷和主動式中斷。

  • 搶先式中斷

不需要線程的執行代碼配合,在發生GC時,首先把所有線程全部中斷,如果發現有線程中斷的地方不在安全點上,就恢復線程,讓它“跑”到安全點上。

現在幾乎沒有虛擬機實現採用搶先式中斷來暫停線程從而響應GC事件

  • 主動式中斷

當GC需要中斷線程的時候,不直接對線程操作,僅僅簡單地設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就自己中斷掛起。

輪詢標誌的地方和安全點是重合的,另外再加上創建對象需要分配內存的地方。

3.4.3.安全區域

安全點機制保證了程序執行時,在不太長的時間內就會遇到可進入GC的安全點,但是,當線程處於Sleep狀態或者“Blocked”狀態,這時候線程沒辦法響應虛擬機的中斷請求。對於這種情況,就需要安全區域(Safe Region)來解決。

安全區域是指在一段代碼片段之中,引用關係不會發生變化。在這個區域中的任意地方開始GC都是安全的。

線程在執行到Safe Region中代碼時,首先標識自己已經進入了Safe Region。當發生GC時,可以忽略標識自己爲Safe Region狀態的線程。當線程要離開Safe Region時,它要檢查系統是否已經完成枚舉根節點(或者整個GC過程),如果完成線程繼續執行,否則必須等待知道收到可以離開的信號。

3.5.垃圾收集器

  • 新生代:Serial,ParNew,Parallel Scavenge
  • 老年代:CMS,Serial Old(MSC),Parallel Old

允許的收集器組合:

  1. Serial + CMS / Serial Old
  2. ParNew + CMS / Serial Old
  3. Parallel Scavenge + Serial Old / Parallel Old
  4. G1

3.5.1.Serial收集器(複製算法)

Serial收集器是最基本,發展歷史最悠久的收集器。

  • 缺點

它只會使用一個CPU或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,知道它收集結束。

  • 優點

簡單高效(與其他收集器的單線程比)。對於限定單個CPU的環境來說,Serial收集器由於沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程收集效率

  • 參數列表
參數 作用
-XX:SurvivorRatio 指定Eden區域的佔比
-XX:PretenureSizeThreshold 大對象直接分配到老年代
-XX:MaxTenuringThreshold 長期存活對象分配到老年代
-XX:+HandlePromotionFailure 空間分配擔保

下圖爲Serial/SerialOld收集器運行示意圖,拍攝於周志明老師的《深入理解Java虛擬機 第2版》
Serial/SerialOld收集器運行示意圖

3.5.2.ParNew收集器(複製算法)

ParNew收集器其實就是Serial收集器的多線程版本,除了使用多線程進行垃圾收集以外,其餘行爲都是一致的。

因爲除了Serial,只有ParNew能和CMS配合工作,所以ParNew成爲運行在Server模式下的新生代收集器的首選。

隨着可以使用的CPU的數量增加,它對於GC時系統資源的有效利用還是很有好處的。它默認開啓的收集線程數與CPU的數量相同。

  • 參數列表
參數 作用
-XX:ParallelGCThreads 垃圾收集的線程數

下圖爲ParNew/SerialOld收集器運行示意圖,拍攝於周志明老師的《深入理解Java虛擬機 第2版》
ParNew/SerialOld收集器運行示意圖

3.5.3.Parallel Scavenge收集器(複製算法)

該收集器的目標是達到一個可控制的吞吐量(Throughput)。吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)。

例如:虛擬機總共運行了100分鐘,其中垃圾收集花掉了1分鐘,那吞吐量=99/100=99%;

停頓時間越短就越適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗,而高吞吐量則可以高效地利用CPU時間,儘快完成程序的運算任務,主要適合在後臺運算而不需要太多交互的任務。

  • 參數列表
參數 作用
-XX:MaxGCPauseMillis 大於0的毫秒數,最大內存回收花費的時間
-XX:GCTimeRatio 大於0且小於100的整數,垃圾收集時間佔總時間的比率,相當於吞吐量的倒數,默認值爲99
-XX:+UseAdaptiveSizePolicy 開啓GC自適應的調節策略。虛擬機會根據當前系統的運行情況動態調整參數

3.5.4.Serial Old收集器(標記整理算法)

Serial Old是Serial收集器的老年代版本,同樣是一個單線程的收集器。主要是給Client模式下的虛擬機使用。如果用於Server模式下,那麼有兩個用途:

  1. 在JDK1.5以及之前的版本中與Parallel Scavenge收集器搭配使用
  2. 作爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用

下圖爲Serial/SerialOld收集器運行示意圖,拍攝於周志明老師的《深入理解Java虛擬機 第2版》
Serial/SerialOld收集器運行示意圖

3.5.5.Parallel Old收集器(標記整理算法)

Parallel Scavenge收集器的老年代版本。

在注重吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器。

下圖爲Parallel Scavebge/Parallel Old收集器運行示意圖,拍攝於周志明老師的《深入理解Java虛擬機 第2版》
Parallel Scavebge/Parallel Old收集器運行示意圖

3.5.6.CMS收集器(標記清除算法)

CMS收集器整個過程分爲4個步驟,其中初始標記和重新標記兩個步驟仍然需要“Stop The World”。

  1. 初始標記(CMS initial mark):僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快。
  2. 併發標記(CMS concurrent mark):進行GC Roots Tracing的過程。
  3. 重新標記(CMS remark):爲了修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄。這個階段的停頓時間會比初始標記階段稍長一些,但是遠比並發標記的時間短。
  4. 併發清除(CMS concurrent sweep)

由於整個過程中最耗時的併發標記和併發清除過程收集器線程都可以與用戶線程一起工作,所以,總體上來說,CMS收集器的內存回收過程是與用戶線程一起併發執行的。

該收集器存在3個明顯的缺點:

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

CMS默認啓動的回收線程數是(CPU數量+3)/4,也就是當CPU在4個以上時,併發回收時垃圾收集線程不少於25%的CPU資源,並且隨着CPU數量的增加而下降。如果CPU只有2個,那要分出一半的運算能力去執行收集器線程。

  1. CMS收集器無法處理浮動垃圾

由於CMS併發清理階段用戶線程還在運行着,伴隨程序運行自然就還有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS無法在檔次收集中處理掉它們,只好留住等待下一次GC時再清理。這一部分垃圾就成爲“浮動垃圾”。

由於垃圾收集階段,用戶線程還需要運行,因此需要預留一部分空間提供併發收集時的程序運作使用(可以通過參數-XX:CMSInitiatingOccupancyFraction設置)。要是CMS運行期間預留的內存無法滿足程序需要,就會出現一次“Concurrent Mode Failure”失敗,這時虛擬機將啓動後備預案:臨時啓用Serial Old收集器來進行老年代的垃圾收集,這樣停頓的時間就很長了。

  1. 空間碎片

CMS是基於標記清除算法,所以在收集結束之後會有大量的空間碎片產生。提供參數來控制進行FullGC時開啓內存碎片的合併整理,還有執行多少次不整理的Full GC後,來一次整理。

  • 參數列表
參數 作用
-XX:CMSInitiatingOccupancyFraction 在老年代空間被使用多少後觸發垃圾收集,默認值爲68%
-XX:UseCMSCompactAcFullCollection 在完成收集後是否要進行一次內存碎片整理。(默認爲開啓)
-XX:CMSFullGCsBeforeCompaction 在進行若干次垃圾收集後再啓動一次內存碎片整理。(默認爲0,表示每次Full GC都進行碎片整理)

下圖爲CMS收集器運行示意圖,拍攝於周志明老師的《深入理解Java虛擬機 第2版》
CMS收集器運行示意圖

3.5.7.G1收集器(複製算法+標記整理算法)

G1(Garbage-First)收集器是一款面向服務端應用的垃圾收集器。分爲以下四個步驟:

  1. 初始標記(Initial Marking)
  2. 併發標記(Concurrent Marking)
  3. 最終標記(Final Marking)
  4. 篩選回收(Live Data Counting and Evacuation)

具備以下特點:

  1. 並行與併發:使用多個CPU來縮短Stop-The-World停頓的時間,通過併發的方式讓收集動作和Java程序同時進行
  2. 分代收集:不需要其他收集器配合,依然採用分代收集方式獨立管理整個堆
  3. 空間整合
  4. 可預測的停頓:這是G1相對於CMS的一大優勢。可預測的停頓是指允許使用者指定在長度爲M毫秒內,消耗在垃圾收集的時間不得超過N毫秒。

下圖爲G1收集器運行示意圖,拍攝於周志明老師的《深入理解Java虛擬機 第2版》
G1收集器運行示意圖

3.5.8.理解GC日誌

參數 功能說明
-XX:+PrintGC 輸出GC日誌
-XX:+PrintGCDetails 輸出GC詳細日誌
-XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準時間的形式)
-XX:+PrintGCDateStamps 輸出GC的時間戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在進行GC的前後打印出堆的信息
-Xloggc:…/logs/gc.log 日誌文件的輸出路徑

爲了打印以下GC日誌,使用到的參數:-Xmx20m -Xms20m -XX:MetaspaceSize=12M -XX:MaxMetaspaceSize=20M -XX:+PrintGCDetails -XX:+PrintGCTimeStamps

5.002: [GC (Allocation Failure) [PSYoungGen: 5920K->288K(6144K)] 10999K->5495K(19968K), 0.0008073 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
5.218: [GC (Allocation Failure) [PSYoungGen: 5920K->288K(6144K)] 11127K->5575K(19968K), 0.0005809 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
5.221: [GC (Metadata GC Threshold) [PSYoungGen: 943K->224K(6144K)] 6231K->5663K(19968K), 0.0010454 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
5.223: [Full GC (Metadata GC Threshold) [PSYoungGen: 224K->0K(6144K)] [ParOldGen: 5439K->3231K(13824K)] 5663K->3231K(19968K), [Metaspace: 11071K->11071K(1060864K)], 0.0266278 secs] [Times: user=0.22 sys=0.02, real=0.03 secs] 
  • 5.002表示GC執行的時間,這個數字的含義是從Java虛擬機啓動以來經過的秒數。
  • GC和Full GC表示這次垃圾收集的停頓類型,Full GC表示這次GC發生了Stop the world。附帶了發生原因
  • PSYoungGen,ParOldGen,Metaspace表示GC發生的區域,這裏顯示的區域名稱與使用的GC收集器是密切相關的。PSYoungGen爲Parallel Scavenge收集器(新生代),ParOldGen爲Parallel Old收集器(老年代),Metaspace是元空間。
  • 5920K->288K(6144K)表示GC前該內存區域已使用容量->GC後該內存區域已使用容量(該內存區域總容量)
  • 括號外面的10999K->5495K(19968K)表示的是GC前該Java堆已使用容量->GC後該Java堆已使用容量(該Java堆總容量)
  • 0.0025925表示該內存區域GC所佔用的時間

3.5.9.垃圾收集器的參數總結

參數 描述
UseSerialGC 虛擬機運行在Client模式下的默認值,打開此開關後,使用Serial+Serial Old的收集器組合進行內存回收
UseParNewGC 打開此開關後,使用ParNew + Serial Old的收集器組合進行內存回收
UseConcMarkSweepGC 打開此開關後,使用ParNew + CMS + Serial Old的收集器組合進行內存回收。Serial Old收集器將作爲CMS收集器出現Concurrent Mode Failure失敗後的後備收集器使用。
UseParallelOldGC 打開此開關後,使用Parallel Scavenge + Parallel Old的收集器組合進行內存回收
SurvivorRatio 新生代中Eden區域與Survivor區域的容量比例,默認爲8,代表Eden:Survivor=8:1.
PretenureSizeThreshold 直接晉升到老年代的對象大小,設置這個參數後,大於這個參數的對象將直接在老年代分配
MaxTenringThreshold 晉升到老年代的對象年齡。每個對象在堅持過一次Minor GC之後,年齡就+1,當超過這個參數值時就進入老年代。
UseAdaptiveSizePolicy 動態調整Java堆中各個區域的大小以及進入老年代的年齡。
HandlePromotionFailure 是否允許分配擔保失敗,即老年代的剩餘空間不足以應付新生代的整個Eden和Survivor區的所有對象都存活的極端情況
ParallelGCThreads 設置並行GC時進行內存回收的線程數
GCTimeRatio GC時間佔總時間的比率,默認值爲99,即允許1%的GC時間。僅在使用Parallel Scavenge收集器時生效
MaxGCPauseMills 設置GC的最大停頓時間。僅在使用Parallel Scavenge收集器時生效
CMSInitiatingOccupancyFraction 設置CMS收集器在老年代空間被使用多少後觸發垃圾收集,默認值爲68%
UseCMSCompactAcFullCollection 設置CMS收集器在完成收集後是否要進行一次內存碎片整理
CMSFullGCsBeforeCompaction 設置CMS收集器在進行若干次垃圾收集後再啓動一次內存碎片整理

3.6.內存分配與回收策略

可以使用-XX:PrintGCDetails參數,實時查看GC日誌。

每個收集器的具體實現不同,得看具體使用的是什麼收集器

3.6.1.對象優先在Eden分配

大多數情況下,對象在新生代Eden區中分配(部分大對象可能直接在老年代中分配)。當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。

以下代碼通過參數-Xms20M和-Xmx20M限制了堆的大小爲20M,參數-Xmn10M限制了新生代的大小爲10M,參數-XX:SurvivorRatio=8定義新生代中Eden區與一個Survivor區的空間比例爲8:1

/**
 * @author guoyu.huang
 * @version 1.0.0
 */
public class TestAllocation {

    private static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {
        // VM args: -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
        byte[] allocation1, allocation2, allocation3, allocation4;

        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation4 = new byte[4 * _1MB];
    }
}

// 打印內容
[GC (Allocation Failure) [PSYoungGen: 6335K->831K(9216K)] 6335K->4935K(19456K), 0.0028907 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 9216K, used 7378K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 79% used [0x00000000ff600000,0x00000000ffc64e60,0x00000000ffe00000)
  from space 1024K, 81% used [0x00000000ffe00000,0x00000000ffecfcb0,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 4104K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 40% used [0x00000000fec00000,0x00000000ff002020,0x00000000ff600000)
 Metaspace       used 3055K, capacity 4556K, committed 4864K, reserved 1056768K
  class space    used 323K, capacity 392K, committed 512K, reserved 1048576K

通過打印的日誌可以看出:

  1. allocation1,allocation2,allocation3對象優先分配在Eden區域。
  2. allocation4因爲新生代無法分配內存,直接進入老年代。

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

大對象是指需要大量連續內存空間的java對象,最典型的大對象就是那種很長的字符串以及數組。可以使用-XX:PretenureSizeThreshold參數,令大於這個設置值的對象直接在老年代分配。這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的內存複製。

PretenureSizeThreshold參數只對Serial和ParNew兩款收集器有效

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

虛擬機給每個對象定義了對象年齡計數器。如果對象在Eden區出生並經過一次Minor GC後仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並且對象年齡設爲1。對象在Survivor區中每經歷一次Minor GC,年齡就增加一歲,當它的年齡增加到一定程度(默認爲15),就將被晉升到老年代中。

3.6.4.動態對象年齡判定

爲了能更好地適應不同程序的內存狀況,虛擬機並不是永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。

3.6.5.空間分配擔保

在發生Minor GC之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那麼Minor GC可以確保是安全的。如果不成立,則虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將嘗試進行一次Minor GC,儘管這次Minor GC有分享,如果小於或者HandlePromotionFailure設置爲不允許冒險,將進行Full GC。

結論

  • 哪些內存需要回收?什麼時候回收?

可用可達性分析算法,當內存不夠時,回收沒有任何引用的對象。

  • 如何回收?

根據三個回收算法:標記清除,標記整理,複製,實現分代收集。每個收集器具體的實現不同。

關注我

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章