天天看點

JVM 判斷對象已死,實踐驗證GC回收

JVM 判斷對象已死,實踐驗證GC回收

作者:小傅哥

部落格:https://bugstack.cn

沉澱、分享、成長,讓自己和他人都能有所收獲!😄

一、前言

提升自身價值有多重要?

經過了風風雨雨,看過了男男女女。時間經過的歲月就沒有永恒不變的!

在這趟車上有人下、有人上,外在别人給你點評的标簽、留下的烙印,都隻是這趟車上的故事。隻有個人成長了、積累了、沉澱了,才有機會當自己的司機。

可能某個年齡段的你還看不懂,但如果某天你不那麼忙了,要思考思考自己的路、自己的腳步。看看這些是不是你想要的,如果都是你想要的,為什麼你看起來不開心?

好!加油,走向你想成為的自己!

二、面試題

謝飛機,小記!

,中午吃飽了開始發呆,怎麼就學不來這些知識呢,它也不進腦子!

謝飛機:喂,面試官大哥,我想問個問題。

面試官:什麼?

謝飛機:就是這知識它不進腦子呀!

面試官:這....

謝飛機:就是看了忘,忘了看的!

面試官:是不是沒有實踐?隻是看了就覺得會了,收藏了就表示懂了?哪哪都不深入!?

謝飛機:好像是!那有什麼辦法?

面試官:也沒有太好的辦法,學習本身就是一件枯燥的事情。減少碎片化的時間浪費,多用在系統化的學習上會更好一些。哪怕你寫寫部落格記錄下,驗證下也是好的。

三、先動手驗證垃圾回收

說是垃圾回收,我不引用了它就回收了?什麼時候回收的?咋回收的?

沒有看到實際的例子,往往就很難讓理科生接受這類知識。我自己也一樣,最好是讓我看得見。代碼是對數學邏輯的具體實作,沒有實作過程隻看答案是沒有意義的。

測試代碼

public class ReferenceCountingGC {

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

    public static void main(String[] args) {
        testGC();
    }

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

}
           

例子來自于《深入了解Java虛拟機》中引用計數算法章節。

例子要說明的結果是,互相引用下卻已經置為null的兩個對象,是否會被GC回收。如果隻是按照引用計數器算法來看,那麼這兩個對象的計數辨別不會為0,也就不能被回收。但到底有沒有被回收呢?

這裡我們先采用 jvm 工具指令,jstat來監控。因為監控的過程需要我手敲代碼,比較耗時,是以我們在調用testGC()前,睡眠會

Thread.sleep(55000);

。啟動代碼後執行如下指令。

E:\itstack\git\github.com\interview>jps -l
10656
88464
38372 org.itstack.interview.ReferenceCountingGC
26552 sun.tools.jps.Jps
110056 org.jetbrains.jps.cmdline.Launcher

E:\itstack\git\github.com\interview>jstat -gc 38372 2000
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
10752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.000
10752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.000
10752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.000
10752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.000
10752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.000
10752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.000
10752.0 10752.0  0.0    0.0   65536.0   6561.4   175104.0     0.0     4480.0 770.9  384.0   75.9       0    0.000   0      0.000    0.000
10752.0 10752.0  0.0   1288.0 65536.0    0.0     175104.0     8.0     4864.0 3982.6 512.0  440.5       1    0.003   1      0.000    0.003
10752.0 10752.0  0.0    0.0   65536.0   437.3    175104.0    1125.5   4864.0 3982.6 512.0  440.5       1    0.003   1      0.012    0.015
10752.0 10752.0  0.0    0.0   65536.0   437.3    175104.0    1125.5   4864.0 3982.6 512.0  440.5       1    0.003   1      0.012    0.015
           
  • S0C、S1C,第一個和第二個幸存區大小
  • S0U、S1U,第一個和第二個幸存區使用大小
  • EC、EU,伊甸園的大小和使用
  • OC、OU,老年代的大小和使用
  • MC、MU,方法區的大小和使用
  • CCSC、CCSU,壓縮類空間大小和使用
  • YGC、YGCT,年輕代垃圾回收次數和耗時
  • FGC、FGCT,老年代垃圾回收次數和耗時
  • GCT,垃圾回收總耗時

注意:觀察後面三行,

S1U = 1288.0

GCT = 0.003

,說明已經在執行垃圾回收。

接下來,我們再換種方式測試。在啟動的程式中,加入GC列印參數,觀察GC變化結果。

-XX:+PrintGCDetails  列印每次gc的回收情況 程式運作結束後列印堆空間記憶體資訊(包含記憶體溢出的情況)
-XX:+PrintHeapAtGC  列印每次gc前後的記憶體情況
-XX:+PrintGCTimeStamps 列印每次gc的間隔的時間戳 full gc為每次對新生代老年代以及整個空間做統一的回收 系統中應該盡量避免
-XX:+TraceClassLoading  列印類加載情況
-XX:+PrintClassHistogram 列印每個類的執行個體的記憶體占用情況
-Xloggc:/Users/xiaofuge/Desktop/logs/log.log  配合上面的使用将上面的日志列印到指定檔案
-XX:HeapDumpOnOutOfMemoryError 發生記憶體溢出将堆資訊轉存起來 以便分析
           

這回就可以把睡眠去掉了,并添加參數

-XX:+PrintGCDetails

,如下:

JVM 判斷對象已死,實踐驗證GC回收

測試結果

[GC (System.gc()) [PSYoungGen: 9346K->936K(76288K)] 9346K->944K(251392K), 0.0008518 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 936K->0K(76288K)] [ParOldGen: 8K->764K(175104K)] 944K->764K(251392K), [Metaspace: 3405K->3405K(1056768K)], 0.0040034 secs] [Times: user=0.08 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 76288K, used 1966K [0x000000076b500000, 0x0000000770a00000, 0x00000007c0000000)
  eden space 65536K, 3% used [0x000000076b500000,0x000000076b6eb9e0,0x000000076f500000)
  from space 10752K, 0% used [0x000000076f500000,0x000000076f500000,0x000000076ff80000)
  to   space 10752K, 0% used [0x000000076ff80000,0x000000076ff80000,0x0000000770a00000)
 ParOldGen       total 175104K, used 764K [0x00000006c1e00000, 0x00000006cc900000, 0x000000076b500000)
  object space 175104K, 0% used [0x00000006c1e00000,0x00000006c1ebf100,0x00000006cc900000)
 Metaspace       used 3449K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 376K, capacity 388K, committed 512K, reserved 1048576K
           
  • 從運作結果可以看出記憶體回收日志,Full GC 進行了回收。
  • 也可以看出JVM并不是依賴引用計數器的方式,判斷對象是否存活。否則他們就不會被回收啦

有了這個例子,我們再接着看看JVM垃圾回收的知識架構!

四、JVM 垃圾回收知識架構

垃圾收集(Garbage Collection,簡稱GC),最早于1960年誕生于麻省理工學院的Lisp是第一門開始使用記憶體動态配置設定和垃圾收集技術的語言。

垃圾收集器主要做的三件事:

哪些記憶體需要回收

什麼時候回收

、怎麼回收。

而從垃圾收集器的誕生到現在有半個世紀的發展,現在的記憶體動态配置設定和記憶體回收技術已經非常成熟,一切看起來都進入了“自動化”。但在某些時候還是需要我們去監測在高并發的場景下,是否有記憶體溢出、洩漏、GC時間過程等問題。是以在了解和知曉垃圾收集的相關知識對于進階程式員的成長就非常重要。

垃圾收集器的核心知識項主要包括:判斷對象是否存活、垃圾收集算法、各類垃圾收集器以及垃圾回收過程。如下圖;

JVM 判斷對象已死,實踐驗證GC回收

原圖下載下傳連結:http://book.bugstack.cn/#s/6jJp2icA

1. 判斷對象已死

1.1 引用計數器

  1. 為每一個對象添加一個引用計數器,統計指向該對象的引用次數。
  2. 當一個對象有相應的引用更新操作時,則對目标對象的引用計數器進行增減。
  3. 一旦當某個對象的引用計數器為0時,則表示此對象已經死亡,可以被垃圾回收。

從實作來看,引用計數器法(Reference Counting)雖然占用了一些額外的記憶體空間來進行計數,但是它的實作方案簡單,判斷效率高,是一個不錯的算法。

也有一些比較出名的引用案例,比如:微軟COM(Component Object Model) 技術、使用ActionScript 3的FlashPlayer、 Python語言等。

但是,在主流的Java虛拟機中并沒有選用引用技術算法來管理記憶體,主要是因為這個簡單的計數方式在處理一些互相依賴、循環引用等就會非常複雜。可能會存在不再使用但又不能回收的記憶體,造成記憶體洩漏

1.2 可達性分析法

Java、C#等主流語言的記憶體管理子系統,都是通過可達性分析(Reachability Analysis)算法來判定對象是否存活的。

它的算法思路是通過定義一系列稱為 GC Roots 根對象作為起始節點集,從這些節點出發,窮舉該集合引用到的全部對象填充到該集合中(live set)。這個過程教過标記,隻标記那些存活的對象 好,那麼現在未被标記的對象就是可以被回收的對象了。

GC Roots 包括;

  1. 全局性引用,對方法區的靜态對象、常量對象的引用
  2. 執行上下文,對 Java方法棧幀中的局部對象引用、對 JNI handles 對象引用
  3. 已啟動且未停止的 Java 線程

兩大問題

  1. 誤報:已死亡對象被标記為存活,垃圾收集不到。多占用一會記憶體,影響較小。
  2. 漏報:引用的對象(正在使用的)沒有被标記為存活,被垃圾回收了。那麼直接導緻的就是JVM奔潰。(STW可以確定可達性分析法的準确性,避免漏報)

2. 垃圾回收算法

2.1 标記-清除算法(mark-sweep)

JVM 判斷對象已死,實踐驗證GC回收
  • 标記無引用的死亡對象所占據的空閑記憶體,并記錄到空閑清單中(free list)。
  • 當需要建立新對象時,記憶體管理子產品會從 free list 中尋找空閑記憶體,配置設定給建立的對象。
  • 這種清理方式其實非常簡單高效,但是也有一個問題記憶體碎片化太嚴重了。
  • Java 虛拟機的堆中對象,必須是連續分布的,是以極端的情況下可能即使總剩餘記憶體充足,但尋找連續記憶體配置設定效率低,或者嚴重到無法配置設定記憶體。重新開機湯姆貓!
  • 在CMS中有此類算法的使用,GC暫停時間短,但存在算法缺陷。

2.2 标記-複制算法(mark-copy)

JVM 判斷對象已死,實踐驗證GC回收
  • 從圖上看這回做完垃圾清理後連續的記憶體空間就大了。
  • 這種方式是把記憶體區域分成兩份,分别用兩個指針 from 和 to 維護,并且隻使用 from 指針指向的記憶體區域配置設定記憶體。
  • 當發生垃圾回收時,則把存活對象複制到 to 指針指向的記憶體區域,并交換 from 與 to 指針。
  • 它的好處很明顯,就是解決記憶體碎片化問題。但也帶來了其他問題,堆空間浪費了一半。

2.3 标記-壓縮算法(mark-compact)

JVM 判斷對象已死,實踐驗證GC回收
  • 1974年,Edward Lueders 提出了标記-壓縮算法,标記的過程和标記清除算法一樣,但在後續對象清理步驟中,先把存活對象都向記憶體空間一端移動,然後在清理掉其他記憶體空間。
  • 這種算法能夠解決記憶體碎片化問題,但壓縮算法的性能開銷也不小。

3. 垃圾回收器

3.1 新生代

  1. Serial
    1. 算法:标記-複制算法
    2. 說明:簡單高效的單核機器,Client模式下預設新生代收集器;
  2. Parallel ParNew
    1. 算法: 标記-複制算法
    2. 說明:GC線程并行版本,在單CPU場景效果不突出。常用于Client模式下的JVM
  3. Parallel Scavenge
    1. 說明:目标在于達到可控吞吐量(吞吐量=使用者代碼運作時間/(使用者代碼運作時間+垃圾回收時間));

3.2 老年代

  1. Serial Old
    1. 算法:标記-壓縮算法
    2. 說明:性能一般,單線程版本。1.5之前與Parallel Scavenge配合使用;作為CMS的後備預案。
  2. Parallel Old
    1. 說明:GC多線程并行,為了替代Serial Old與Parallel Scavenge配合使用。
  3. CMS
    1. 算法:标記-清除算法
    2. 說明:對CPU資源敏感、停頓時間長。标記-清除算法,會産生記憶體碎片,可以通過參數開啟碎片的合并整理。基本已被G1取代

3.3 G1

  1. 說明:适用于多核大記憶體機器、GC多線程并行執行,低停頓、高回收效率。

五、總結

  • JVM 的關于自動記憶體管理的知識衆多,包括本文還沒提到的 HotSpot 實作算法細節的相關知識,包括:安全節點、安全區域、卡表、寫屏障等。每一項内容都值得深入學習。
  • 如果不僅僅是為了面試背題,最好的方式是實踐驗證學習。否則這類知識就像3分以下的過電影一樣,很難記住它的内容。
  • 整個的内容也是小傅哥學習整理的一個過程,後續還會不斷的繼續深挖和分享。感興趣的小夥伴可以一起讨論學習。

六、系列推薦

  • 認知自己的技術棧盲區
  • 為了搞清楚類加載,竟然手撸JVM!
  • JVM故障處理工具,使用總結
  • Thread.start() ,它是怎麼讓線程啟動的呢?
  • ThreadLocal 你要這麼問,我就挂了!

公衆号:bugstack蟲洞棧 | 作者小傅哥多年從事一線網際網路 Java 開發的學習曆程技術彙總,旨在為大家提供一個清晰詳細的學習教程,側重點更傾向編寫Java核心内容。如果能為您提供幫助,請給予支援(關注、點贊、分享)!