JVM 判斷對象已死,實踐驗證GC回收
本文轉載自微信公眾號「 bugstack蟲洞?!?,作者小傅哥 。轉載本文請聯系 bugstack蟲洞棧公眾號。
目錄
- 一、前言
- 二、面試題
- 三、先動手驗證垃圾回收
- 四、JVM 垃圾回收知識框架
- 1. 判斷對象已死
- 2. 垃圾回收算法
- 3. 垃圾回收器
- 五、總結
- 六、系列推薦
一、前言
提升自身價值有多重要?
經過了風風雨雨,看過了男男女女。時間經過的歲月就沒有永恒不變的!
在這趟車上有人下、有人上,外在別人給你點評的標簽、留下的烙印,都只是這趟車上的故事。只有個人成長了、積累了、沉淀了,才有機會當自己的司機。
可能某個年齡段的你還看不懂,但如果某天你不那么忙了,要思考思考自己的路、自己的腳步??纯催@些是不是你想要的,如果都是你想要的,為什么你看起來不開心?
好!加油,走向你想成為的自己!
二、面試題
謝飛機,小記!,中午吃飽了開始發呆,怎么就學不來這些知識呢,它也不進腦子!
「謝飛機」:喂,面試官大哥,我想問個問題。
「面試官」:什么?
「謝飛機」:就是這知識它不進腦子呀!
「面試官」:這....
「謝飛機」:就是看了忘,忘了看的!
「面試官」:是不是沒有實踐?只是看了就覺得會了,收藏了就表示懂了?哪哪都不深入!?
「謝飛機」:好像是!那有什么辦法?
「面試官」:也沒有太好的辦法,學習本身就是一件枯燥的事情。減少碎片化的時間浪費,多用在系統化的學習上會更好一些。哪怕你寫寫博客記錄下,驗證下也是好的。
三、先動手驗證垃圾回收
說是垃圾回收,我不引用了它就回收了?什么時候回收的?咋回收的?
沒有看到實際的例子,往往就很難讓理科生接受這類知識。我自己也一樣,最好是讓我看得見。代碼是對數學邏輯的具體實現,沒有實現過程只看答案是沒有意義的。
「測試代碼」
- 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
- 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,如下:
「測試結果」
- [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時間過程等問題。所以在了解和知曉垃圾收集的相關知識對于高級程序員的成長就非常重要。
垃圾收集器的核心知識項主要包括:判斷對象是否存活、垃圾收集算法、各類垃圾收集器以及垃圾回收過程。如下圖;
圖 27-1 垃圾收集器知識框架
原圖下載鏈接:http://book.bugstack.cn/#s/6jJp2icA
1. 判斷對象已死
1.1 引用計數器
- 為每一個對象添加一個引用計數器,統計指向該對象的引用次數。
- 當一個對象有相應的引用更新操作時,則對目標對象的引用計數器進行增減。
- 一旦當某個對象的引用計數器為0時,則表示此對象已經死亡,可以被垃圾回收。
從實現來看,引用計數器法(Reference Counting)雖然占用了一些額外的內存空間來進行計數,但是它的實現方案簡單,判斷效率高,是一個不錯的算法。
也有一些比較出名的引用案例,比如:微軟COM(Component Object Model) 技術、使用ActionScript 3的FlashPlayer、 Python語言等。
「但是」,在主流的Java虛擬機中并沒有選用引用技術算法來管理內存,主要是因為這個簡單的計數方式在處理一些相互依賴、循環引用等就會非常復雜??赡軙嬖诓辉偈褂玫植荒芑厥盏膬却?,造成內存泄漏
1.2 可達性分析法
Java、C#等主流語言的內存管理子系統,都是通過可達性分析(Reachability Analysis)算法來判定對象是否存活的。
它的算法思路是通過定義一系列稱為 GC Roots 根對象作為起始節點集,從這些節點出發,窮舉該集合引用到的全部對象填充到該集合中(live set)。這個過程教過標記,只標記那些存活的對象 好,那么現在未被標記的對象就是可以被回收的對象了。
GC Roots 包括;
- 全局性引用,對方法區的靜態對象、常量對象的引用
- 執行上下文,對 Java方法棧幀中的局部對象引用、對 JNI handles 對象引用
- 已啟動且未停止的 Java 線程
「兩大問題」
誤報:已死亡對象被標記為存活,垃圾收集不到。多占用一會內存,影響較小。
漏報:引用的對象(正在使用的)沒有被標記為存活,被垃圾回收了。那么直接導致的就是JVM奔潰。(STW可以確??蛇_性分析法的準確性,避免漏報)
2. 垃圾回收算法
2.1 標記-清除算法(mark-sweep)
標記-清除算法(mark-sweep)
- 標記無引用的死亡對象所占據的空閑內存,并記錄到空閑列表中(free list)。
- 當需要創建新對象時,內存管理模塊會從 free list 中尋找空閑內存,分配給新建的對象。
- 這種清理方式其實非常簡單高效,但是也有一個問題內存碎片化太嚴重了。
- 「Java 虛擬機的堆中對象」,必須是連續分布的,所以極端的情況下可能即使總剩余內存充足,但尋找連續內存分配效率低,或者嚴重到無法分配內存。重啟湯姆貓!
- 在CMS中有此類算法的使用,GC暫停時間短,但存在算法缺陷。
2.2 標記-復制算法(mark-copy)
標記-復制算法(mark-copy)
- 從圖上看這回做完垃圾清理后連續的內存空間就大了。
- 這種方式是把內存區域分成兩份,分別用兩個指針 from 和 to 維護,并且只使用 from 指針指向的內存區域分配內存。
- 當發生垃圾回收時,則把存活對象復制到 to 指針指向的內存區域,并交換 from 與 to 指針。
- 它的好處很明顯,就是解決內存碎片化問題。但也帶來了其他問題,堆空間浪費了一半。
2.3 標記-壓縮算法(mark-compact)
標記-壓縮算法(mark-compact)
- 1974年,Edward Lueders 提出了標記-壓縮算法,標記的過程和標記清除算法一樣,但在后續對象清理步驟中,先把存活對象都向內存空間一端移動,然后在清理掉其他內存空間。
- 這種算法能夠解決內存碎片化問題,但壓縮算法的性能開銷也不小。
3. 垃圾回收器
3.1 新生代
1.Serial
算法:標記-復制算法
說明:簡單高效的單核機器,Client模式下默認新生代收集器;
2.Parallel ParNew
算法:標記-復制算法
說明:GC線程并行版本,在單CPU場景效果不突出。常用于Client模式下的JVM
3.Parallel Scavenge
算法:標記-復制算法
說明:目標在于達到可控吞吐量(吞吐量=用戶代碼運行時間/(用戶代碼運行時間+垃圾回收時間));
3.2 老年代
1.Serial Old
算法:標記-壓縮算法
說明:性能一般,單線程版本。1.5之前與Parallel Scavenge配合使用;作為CMS的后備預案。
2.Parallel Old
算法:標記-壓縮算法
說明:GC多線程并行,為了替代Serial Old與Parallel Scavenge配合使用。
3.CMS
算法:標記-清除算法
說明:對CPU資源敏感、停頓時間長。標記-清除算法,會產生內存碎片,可以通過參數開啟碎片的合并整理?;疽驯籊1取代
3.3 G1
算法:標記-壓縮算法
說明:適用于多核大內存機器、GC多線程并行執行,低停頓、高回收效率。
五、總結
JVM 的關于自動內存管理的知識眾多,包括本文還沒提到的 HotSpot 實現算法細節的相關知識,包括:安全節點、安全區域、卡表、寫屏障等。每一項內容都值得深入學習。
如果不僅僅是為了面試背題,最好的方式是實踐驗證學習。否則這類知識就像3分以下的過電影一樣,很難記住它的內容。
整個的內容也是小傅哥學習整理的一個過程,后續還會不斷的繼續深挖和分享。感興趣的小伙伴可以一起討論學習。