深刻理解Java虛擬機3——垃圾回收

《深刻理解Java虛擬機》第3章讀書筆記java

本文介紹瞭如何判斷對象是否存活,三種垃圾回收算法,分析比較了幾種垃圾收集器的特色。本文並不是原創,是《深刻理解Java虛擬機》第3章的整理、總結和補充。算法

對象已死?

垃圾收集器在對堆進行回收前,要先判斷哪些對象「存活」,哪些已經「死去」。json

引用計數算法

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器就加1;當引用失效時,計數器就減1;任什麼時候刻計數器爲0的對象就是不可能再被使用的。安全

主流的Java虛擬機裏面沒有選用引用計數算法來管理內存。服務器

優勢:實現簡單,效率高。微信

缺點:很難解決對象之間相互循環引用的問題。多線程

循環引用問題,以下代碼所示,併發

/** * 源代碼出自《深刻理解Java虛擬機》P62-63 * 循環引用 **/
public class ReferenceCountingGC {

    public Object instance = null;

    private static final int _1MB = 1024 * 1024;

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

    /** * 運行參數 * -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M */
    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();
    }
}
複製代碼

JVM參數設置了新生代爲10MB,運行結果顯示,在第一次觸發GC時,「5120K->576K(9216K)」回收了約4MB內存。意味着虛擬機並無由於兩個對象相互引用就不回收它們,這也側面說明了虛擬機並非經過引用計數算法來判斷對象是否存活網站

可達性分析算法

經過一系列的稱爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來講,就是從GC Roots到這個對象不可達)時,則證實此對象是不可用的。spa

如圖,object五、object六、object7 爲可回收對象

主流的Java虛擬機使用可達性分析算法

在Java語言中,GC Roots包括如下幾種:

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

四種引用

  1. 強引用:代碼中廣泛存在,垃圾收集器不會回收強引用的對象。
  2. 軟引用:有用但非必需,在系統將要發生內存溢出異常以前,會把這些對象列入回收範圍。
  3. 弱引用:非必需,不管當前內存是否足夠,下次垃圾回收都會回收掉這些對象。
  4. 虛引用:最弱的引用關係,是否有虛引用不對其生存時間構成影響。

垃圾收集算法

標記-清除算法

最基礎的收集算法——「標記-清除」(Mark-Sweep)算法。通常用於老年代。

算法分爲「標記」和「清除」兩個階段:

  1. 標記出須要回收的對象
  2. 清除被標記的對象

缺點:效率低,空間碎片化。

複製算法

爲了解決效率問題,出現了「複製」算法(Copying)。它將內存分爲兩塊,每次只使用其中一塊,當這一塊內存用完了,就將還存活的對象複製到另外一塊上面,而後再把已使用過的內存空間一次清理掉。

缺點:內存縮小爲原來的一半。

JVM虛擬機在新生代使用這種收集算法,並非按照 1:1 的比例來劃份內存空間。而是根據新生代中的對象98%是「朝生夕死」這一特色,將內存分爲一塊較大的 Eden(80%) 空間和兩塊較小的 Survivor(10%) 空間。每次只使用 Eden 和其中一塊 Survivor,當回收時,將 Eden 和 Survivor 中還存活的對象,一次性地複製到另一塊 Survivor 空間上,最後清理掉 Eden 和剛纔用過的 Survivor 空間。

HotSpot默認Eden和Survivor的大小比例爲8:1,這樣就只有10%的內存被「浪費」。當出現超過10%的對象存活時,就會使用老年代作分配擔保,把Survivor空間放不下的對象,直接放入老年代。

標記-整理算法

在Mark-Sweep算法的基礎上作了改良,用於解決空間碎片化問題。標記-整理(Mark-Compact)算法在標記後不是簡單作清除,而是讓全部存活的對象都向一端移動,而後清理掉端邊界之外的內存。通常用於老年代。

安全點和安全區域

安全點

在作可達性分析時,須要保持分析期間整個系統不會發生變化,這就致使GC進行時必須停頓全部Java執行線程(Stop The World),即便是在號稱(幾乎)不會發生停頓的CMS收集器中,枚舉根節點時也必需要停頓。

程序執行時並不是在全部地方都能停下來開始GC,只有在到達**安全點(Safepoint)**時才能暫停。Safepoint 的選定既不能太少以至於讓GC等待時間太長,也不能過於頻繁以至於過度增大運行時的負荷。因此,安全點的選定基本上是以程序「是否具備讓程序長時間執行的特徵」爲標準進行選定的,例如方法調用,循環跳轉,異常跳轉等。

如何在GC發生時讓線程都跑到最近的安全點再停頓下來?

  • 搶先試中斷:先把全部線程中斷,發現不在安全點的線程恢復線程,讓它跑到安全點。
  • 主動式中斷:設置一個不可讀的內存位置做爲中斷標誌,標誌與安全點重合,當線程執行到這個標誌時本身中斷掛起。

安全區域

安全區域(Safe Region)是指在一段代碼片斷中,引用關係不會發生變化。在這個區域的任何地方開始GC都是安全的。典型的安全區域好比線程處於Sleep狀態或者Blocked狀態。

在線程執行到Safe Region中的代碼時,首先標識本身已經進入了Safe Region。當要發起GC時,就不用管標識爲Safe Region狀態的線程了。當線程要離開Safe Region時,要檢查是否處於GC狀態,若是是,就要繼續等待,直到收到能夠安全離開Safe Region的信號爲止。

垃圾收集器

並行與併發

  • 並行(Parallel):指多條垃圾收集線程並行工做,但此時用戶線程仍然處於等待狀態。
  • 併發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不必定是並行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集程序運行於另外一個CPU上。

吞吐量

吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即

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

假設虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。

Minor GC 和 Full GC

  • 新生代GC(Minor GC):指發生在新生代的垃圾收集動做,由於Java對象大多都具有朝生夕滅的特性,因此Minor GC 很是頻繁,通常回收速度也比較快。
  • 老年代GC(Major GC / Full GC):指發生在老年代的GC,出現了Major GC,常常會伴隨至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略裏就有直接進行Major GC的策略選擇過程)。Major GC的速度通常會比Minor GC慢10倍以上。

HotSpot虛擬機的垃圾收集器對比

收集器 串行、並行、併發 新生代、老生代 算法 目標 使用場景
Serial 單線程,串行 新生代 複製算法 響應速度優先 單CPU環境下的Client模式
Serial Old 單線程,串行 老年代 標記-整理 響應速度優先 單CPU環境下的Client模式、CMS的後
ParNew 多線程,並行 新生代 複製算法 響應速度優先 多CPU環境時在Server模式下與CMS配合
Parallel Scanvenge 多線程,並行 新生代 複製算法 吞吐量優先 在後臺運算而不須要太多交互的任務
Parallel Old 多線程,並行 老年代 標記-整理 吞吐量優先 在後臺運算而不須要太多交互的任務
CMS 併發 老年代 標記-清除 響應速度優先 集中在互聯網站或B/S系統服務端上的Java應用
G1 併發 both 複製算法+標記-整理 響應速度優先 面向服務端應用,未來替換CMS

ParNew 收集器

Serial收集器的多線程版本,Service模式下的首選新生代收集器,除了Serial收集器外,目前只有它能與CMS收集器配合工做。

ParNew 收集器是使用 -XX:+UseConcMarkSweepGC 選項後的默認新生代收集器,也可使用 -XX:+UseParNewGC 選項來強制指定它。

運行示意圖以下:

CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。基於Mark-Sweep算法。運行過程分爲四個部分:

  • 初始標記
  • 併發標記
  • 從新標記
  • 併發清除

其中,初始標記和從新標記仍然須要 Stop The World。初始標記只是標記一下 GC Roots 能直接關聯到的對象,速度很快。併發標記就是進行 GC Roots Tracing 的過程,而從新標記階段則是爲了修正併發標記期間用戶程序繼續運做而致使標記產生變更的那一部分對象的標記記錄,這個階段的停頓時間比初始標記稍長一些,但遠比並發標記的時間短。

因爲整個過程當中耗時最長的併發標記和併發清楚過程收集器線程均可以與用戶線程一塊兒工做,因此,從整體上來講,CMS收集器的內存回收過程是與用戶線程一塊兒併發執行的。

運行示意圖以下:

CMS 收集器有以下3個缺點:

  1. 對CPU資源很是敏感

    由於併發階段須要佔用一個用戶線程,若是CPU小於4個,則會致使用戶程序的執行速度降低大於25%,若是隻有2個CPU,用戶程序執行速度則會降低50%,這是讓人沒法接收的。通常來講使用CMS收集器的服務器配置至少須要4個CPU。

  2. 沒法處理浮動垃圾

    在併發清理過程當中產生的垃圾稱爲「浮動垃圾」。這些垃圾只能等待下次垃圾回收。所以,CMS 收集器不能像其餘收集器那樣等到老年代幾乎被徹底填滿了再進行收集,須要預留一部分空間提供併發收集時的程序運做使用。

  3. 內存空間碎片化

    CMS 收集器是基於Mark-Sweep算法,這個算法會產生內存空間碎片。CMS 收集器提供了一個 -XX:+UseCMSCompactAtFullCollection 開關參數(默認爲開啓),用於在CMS收集器頂不住要進行Full GC時開啓內存碎片的合併整理過程。內存整理的過程是沒法併發執行的,空間碎片問題沒有了,但停頓時間不得不變長。

使用多個收集器配置,JVM會怎麼處理?

如過同時使用了四個組合配置,這是時候就會報錯

可是好比 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC 這兩個配置同時存在就不會報錯。

翻看源碼可知

有些配置項是能夠並存的。

其實,在使用 UseConcMarkSweepGC 配置的時候,虛擬機默認開啓了 UseParNewGC

因此在配置JVM時,咱們儘可能顯式配置。好比要啓用 ParNew + CMS 組合能夠配置爲

-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
複製代碼

理解GC日誌

《深刻理解Java虛擬機》閱讀筆記系列

深刻理解Java虛擬機1——內存區域

深刻理解Java虛擬機2——對象探祕

深刻理解Java虛擬機3——垃圾回收

本文首發於個人我的博客 chaohang.top

做者 張小超

公衆號【超超不會飛】

轉載請註明出處

歡迎關注個人微信公衆號 【超超不會飛】,獲取第一時間的更新。

相關文章
相關標籤/搜索