深挖Jvm垃圾收集

垃圾收集(Garbage Collection,GC),它的任務是解決如下 3 件問題:java

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

其中第一個問題很好回答,在 Java 中,GC 主要發生在 Java 堆和方法區中,對於後兩個問題,咱們將在以後的內容中進行討論,並介紹 HotSpot 的 7 個垃圾收集器。git

  • 垃圾收集 (GC)
    • 判斷對象的生死
      • 判斷對象是否可用的算法
        • 引用計數算法
        • 可達性分析算法(主流)
      • 四種引用類型
      • 宣告對象死亡的兩次標記過程
      • 方法區的回收
    • 垃圾收集算法
      • 基礎:標記 - 清除算法
      • 解決效率問題:複製算法
      • 解決空間碎片問題:標記 - 整理算法
      • 進化:分代收集算法
    • HotSpot 中 GC 算法的實現
    • 7 個垃圾收集器
      • Serial / ParNew 搭配 Serial Old 收集器
      • Parallel 搭配 Parallel Scavenge 收集器
      • CMS 收集器
      • G1 收集器
    • GC 日誌解讀

判斷對象的生死

判斷對象的生死.png

何時回收對象?固然是這個對象不再會被用到的時候回收。因此要想解決 「何時回收?」 這個問題,咱們要先能判斷一個對象何時何時真正的 「死」 掉了,判斷對象是否可用主要有如下兩種方法。github

判斷對象是否可用的算法

引用計數算法

  • 算法描述:
    • 給對象添加一個引用計數器;
    • 每有一個地方引用它,計數器加 1;
    • 引用失效時,計數器減 1;
    • 計數器值爲 0 的對象再也不可用。
  • 缺點:
    • 很難解決循環引用的問題。即 objA.instance = objB; objB.instance = objA;,objA 和 objB 都不會再被訪問後,它們仍然相互引用着對方,因此它們的引用計數器不爲 0,將永遠不能被判爲不可用。

可達性分析算法(主流)

  • 算法描述:
    • 從 "GC Root" 對象做爲起點開始向下搜索,走過的路徑稱爲引用鏈(Reference Chain);
    • 從 "GC Root" 開始,不可達的對象被判爲不可用。
  • Java 中可做爲 「GC Root」 的對象:
    • 棧中(本地變量表中的reference)
      • 虛擬機棧中,棧幀中的本地變量表引用的對象;
      • 本地方法棧中,JNI 引用的對象(native方法);
    • 方法區中
      • 類的靜態屬性引用的對象;
      • 常量引用的對象;

即使如此,一個對象也不是一旦被判爲不可達,就當即死去的,宣告一個的死亡須要通過兩次標記過程。算法

四種引用類型

JDK 1.2 後,Java 中才有了後 3 種引用的實現。安全

  • 強引用: 像 Object obj = new Object() 這種,只要強引用還存在,垃圾收集器就永遠不會回收掉被引用的對象。
  • 軟引用: 用來引用還存在但非必須的對象。對於軟引用對象,在 OOM 前,虛擬機會把這些對象列入回收範圍中進行第二次回收,若是此次回收後,內存仍是不夠用,就 OOM。實現類:SoftReference
  • 弱引用: 被弱引用引用的對象只能生存到下一次垃圾收集前,一旦發生垃圾收集,被弱引用所引用的對象就會被清掉。實現類:WeakReference
  • 虛引用: 幽靈引用,對對象沒有半毛錢影響,甚至不能用來取得一個對象的實例。它惟一的用途就是:當被一個虛引用引用的對象被回收時,系統會收到這個對象被回收了的通知。實現類:PhantomReference

宣告對象死亡的兩次標記過程

  • 當發現對象不可達後,該對象被第一次標記,並進行是否有必要執行 finalize() 方法的判斷;
    • 不須要執行:對象沒有覆蓋 finalize() 方法,或者 finalize() 方法已被執行過(finalize() 只被執行一次);
    • 須要執行:將該對象放置在一個隊列中,稍後由一個虛擬機自動建立的低優先級線程執行。
  • finalize() 方法是對象逃脫死亡的最後一次機會,不過虛擬機不保證等待 finalize() 方法執行結束,也就是說,虛擬機只觸發 finalize() 方法的執行,若是這個方法要執行超久,那麼虛擬機並不等待它執行結束,因此最好不要用這個方法。
  • finalize() 方法能作的,try-finally 都能作,因此忘了這個方法吧!

方法區的回收

永久代的 GC 主要回收:廢棄常量 和 無用的類數據結構

  • 廢棄常量:例如一個字符串 "abc",當沒有任何引用指向 "abc" 時,它就是廢棄常量了。
  • 無用的類:同時知足如下 3 個條件的類。
    • 該類的全部實例已被回收,Java 堆中不存在該類的任何實例;
    • 加載該類的 Classloader 已被回收;
    • 該類的 Class 對象沒有被任何地方引用,即沒法在任何地方經過反射訪問該類的方法。

垃圾收集算法

垃圾收集算法.png

基礎:標記 - 清除算法

  • 算法描述:
    • 先標記出全部須要回收的對象(圖中深色區域);
    • 標記完後,統一回收全部被標記對象(留下狗啃似的可用內存區域……)。
  • 不足:
    • 效率問題:標記和清理兩個過程的效率都不高。
    • 空間碎片問題:標記清除後會產生大量不連續的內存碎片,致使之後爲較大的對象分配內存時找不到足夠的連續內存,會提早觸發另外一次 GC。

標記清除GC算法.png

解決效率問題:複製算法

  • 算法描述:多線程

    • 將可用內存分爲大小相等的兩塊,每次只使用其中一塊;
    • 當一塊內存用完時,將這塊內存上還存活的對象複製到另外一塊內存上去,將這一塊內存所有清理掉。
  • 不足: 可用內存縮小爲原來的一半,適合GC事後只有少許對象存活的新生代。架構

  • 節省內存的方法:併發

    • 新生代中的對象 98% 都是朝生夕死的,因此不須要按照 1:1 的比例對內存進行劃分;
    • 把內存劃分爲:
      • 1 塊比較大的 Eden 區;
      • 2 塊較小的 Survivor 區;
    • 每次使用 Eden 區和 1 塊 Survivor 區;
    • 回收時,將以上 2 部分區域中的存活對象複製到另外一塊 Survivor 區中,而後將以上兩部分區域清空;
    • JVM 參數設置:-XX:SurvivorRatio=8 表示 Eden 區大小 / 1 塊 Survivor 區大小 = 8

複製GC算法.png

解決空間碎片問題:標記 - 整理算法

  • 算法描述:
    • 標記方法與 「標記 - 清除算法」 同樣;
    • 標記完後,將全部存活對象向一端移動,而後直接清理掉邊界之外的內存。
  • 不足: 存在效率問題,適合老年代。

標記整理GC算法.png

進化:分代收集算法

  • 新生代: GC 事後只有少許對象存活 —— 複製算法
  • 老年代: GC 事後對象存活率高 —— 標記 - 整理算法

HotSpot 中 GC 算法的實現

經過前兩小節對於判斷對象生死和垃圾收集算法的介紹,咱們已經對虛擬機是進行 GC 的流程有了一個大體的瞭解。可是,在 HotSpot 虛擬機中,高效的實現這些算法也是一個須要考慮的問題。因此,接下來,咱們將研究一下 HotSpot 虛擬機究竟是如何高效的實現這些算法的,以及在實現中有哪些須要注意的問題。jvm

GC的算法實現.png

經過以前的分析,GC 算法的實現流程簡單的來講分爲如下兩步:

  1. 找到死掉的對象;
  2. 把它清了。

想要找到死掉的對象,咱們就要進行可達性分析,也就是從 GC Root 找到引用鏈的這個操做。

也就是說,進行可達性分析的第一步,就是要枚舉 GC Roots,這就須要虛擬機知道哪些地方存放着對象應用。若是每一次枚舉 GC Roots 都須要把整個棧上位置都遍歷一遍,那可就費時間了,畢竟並非全部位置都存放在引用呀。因此爲了提升 GC 的效率,HotSpot 使用了一種 OopMap 的數據結構,OopMap 記錄了棧上本地變量到堆上對象的引用關係,也就是說,GC 的時候就不用遍歷整個棧只遍歷每一個棧的 OopMap 就好了。

在 OopMap 的幫助下,HotSpot 能夠快速準確的完成 GC 枚舉了,不過,OopMap 也不是萬年不變的,它也是須要被更新的,當內存中的對象間的引用關係發生變化時,就須要改變 OopMap 中的相應內容。但是能致使引用關係發生變化的指令很是之多,若是咱們執行完一條指令就改下 OopMap,這 GC 成本實在過高了。

所以,HotSpot 採用了一種在 「安全點」 更新 OopMap 的方法,安全點的選取既不能讓 GC 等待的時間過長,也不能過於頻繁增長運行負擔,也就是說,咱們既要讓程序運行一段時間,又不能讓這個時間太長。咱們知道,JVM 中每條指令執行的是很快的,因此一個超級長的指令流也可能很快就執行完了,因此 真正會出現 「長時間執行」 的通常是指令的複用,例如:方法調用、循環跳轉、異常跳轉等,虛擬機通常會將這些地方設置爲安全點更新 OopMap 並判斷是否須要進行 GC 操做。

此外,在進行枚舉根節點的這個操做時,爲了保證準確性,咱們須要在一段時間內 「凍結」 整個應用,即 Stop The World(傳說中的 GC 停頓),由於若是在咱們分析可達性的過程當中,對象的引用關係還在變來變去,那是不可能獲得正確的分析結果的。即使是在號稱幾乎不會發生停頓的 CMS 垃圾收集器中,枚舉根節點時也是必需要停頓的。這裏就涉及到了一個問題:

咱們讓全部線程跑到最近的安全點再停頓下來進行 GC 操做呢?

主要有如下兩種方式:

  • 搶先式中斷:
    • 先中斷全部線程;
    • 發現有線程沒中斷在安全點,恢復它,讓它跑到安全點。
  • 主動式中斷: (主要使用)
    • 設置一箇中斷標記;
    • 每一個線程到達安全點時,檢查這個中斷標記,選擇是否中斷本身。

除此安全點以外,還有一個叫作 「安全區域」 的東西,一個一直在執行的線程能夠本身 「走」 到安全點去,但是一個處於 Sleep 或者 Blocked 狀態的線程是沒辦法本身到達安全點中斷本身的,咱們總不能讓 GC 操做一直等着這些個 」不執行「 的線程從新被分配資源吧。對於這種狀況,咱們要依靠安全區域來解決。

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

當線程執行到安全區域時,它會把本身標識爲 Safe Region,這樣 JVM 發起 GC 時是不會理會這個線程的。當這個線程要離開安全區域時,它會檢查系統是否在 GC 中,若是不在,它就繼續執行,若是在,它就等 GC 結束再繼續執行。

本小節咱們主要講述 HotSpot 虛擬機是如何發起內存回收的,也就是如何找到死掉的對象,至於如何清掉這些個對象,HotSpot 將其交給了一堆叫作 」GC 收集器「 的東西,這東西又有好多種,不一樣的 GC 收集器的處理方式不一樣,適用的場景也不一樣,咱們將在下一小節進行詳細講述。

7 個垃圾收集器

垃圾收集器就是內存回收操做的具體實現,HotSpot 裏足足有 7 種,爲啥要弄這麼多,由於它們各有各的適用場景。有的屬於新生代收集器,有的屬於老年代收集器,因此通常是搭配使用的(除了萬能的 G1)。關於它們的簡單介紹以及分類請見下圖。

垃圾收集器們.png

Serial / ParNew 搭配 Serial Old 收集器

Serial_ParNew收集器.jpg

Serial 收集器是虛擬機在 Client 模式下的默認新生代收集器,它的優點是簡單高效,在單 CPU 模式下很牛。

ParNew 收集器就是 Serial 收集器的多線程版本,雖然除此以外沒什麼創新之處,但它倒是許多運行在 Server 模式下的虛擬機中的首選新生代收集器,由於除了 Serial 收集器外,只有它能和 CMS 收集器搭配使用。

Parallel 搭配 Parallel Scavenge 收集器

首先,這倆貨確定是要搭配使用的,不只僅如此,它倆還賊特別,它們的關注點與其餘收集器不一樣,其餘收集器關注於盡量縮短垃圾收集時用戶線程的停頓時間,而 Parallel Scavenge 收集器的目的是達到一個可控的吞吐量。

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

所以,Parallel Scavenge 收集器無論是新生代仍是老年代都是多個線程同時進行垃圾收集,十分適合於應用在注重吞吐量以及 CPU 資源敏感的場合。

可調節的虛擬機參數:

  • -XX:MaxGCPauseMillis:最大 GC 停頓的秒數;
  • -XX:GCTimeRatio:吞吐量大小,一個 0 ~ 100 的數,最大 GC 時間佔總時間的比率 = 1 / (GCTimeRatio + 1)
  • -XX:+UseAdaptiveSizePolicy:一個開關參數,打開後就無需手工指定 -Xmn-XX:SurvivorRatio 等參數了,虛擬機會根據當前系統的運行狀況收集性能監控信息,自行調整。

CMS 收集器

CMS垃圾收集器.png

CMS收集器運行示意圖.jpg

參數設置:

  • -XX:+UseCMSCompactAtFullCollection:在 CMS 要進行 Full GC 時進行內存碎片整理(默認開啓)
  • -XX:CMSFullGCsBeforeCompaction:在多少次 Full GC 後進行一次空間整理(默認是 0,即每一次 Full GC 後都進行一次空間整理)

關於 CMS 使用 標記 - 清除 算法的一點思考:

以前對於 CMS 爲何要採用 標記 - 清除 算法十分的不理解,既然已經有了看起來更高級的 標記 - 整理 算法,那 CMS 爲何不用呢?最近想了想,感受多是這個緣由,不過也不是很肯定,只是我的的一種猜想。

標記 - 整理 會將全部存活對象向一端移動,而後直接清理掉邊界之外的內存。這就意味着須要一個指針來維護這個分隔存活對象和無用空間的點,而咱們知道 CMS 是併發清理的,雖然咱們啓動了多個線程進行垃圾回收,不過若是使用 標記 - 整理 算法,爲了保證線程安全,在整理時要對那個分隔指針加鎖,保證同一時刻只有一個線程能修改它,加鎖的這一過程至關於將並行的清理過程變成了串行的,也就失去了並行清理的意義了。

因此,CMS 採用了 標記 - 清除 算法。

G1 收集器

G1垃圾收集器.png

G1收集器運行示意圖.jpg

GC 日誌解讀

GC日誌解讀.png

原文:Java架構筆記

免費Java高級資料須要本身領取,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高併發分佈式等教程,一共30G。
傳送門: https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q
相關文章
相關標籤/搜索