JVM 判斷對象已死,實(shí)踐驗(yàn)證GC回收
本文轉(zhuǎn)載自微信公眾號「 bugstack蟲洞?!梗髡咝「蹈?nbsp; 。轉(zhuǎn)載本文請聯(lián)系 bugstack蟲洞棧公眾號。
目錄
- 一、前言
- 二、面試題
- 三、先動(dòng)手驗(yàn)證垃圾回收
- 四、JVM 垃圾回收知識(shí)框架
- 1. 判斷對象已死
- 2. 垃圾回收算法
- 3. 垃圾回收器
- 五、總結(jié)
- 六、系列推薦
一、前言
提升自身價(jià)值有多重要?
經(jīng)過了風(fēng)風(fēng)雨雨,看過了男男女女。時(shí)間經(jīng)過的歲月就沒有永恒不變的!
在這趟車上有人下、有人上,外在別人給你點(diǎn)評的標(biāo)簽、留下的烙印,都只是這趟車上的故事。只有個(gè)人成長了、積累了、沉淀了,才有機(jī)會(huì)當(dāng)自己的司機(jī)。
可能某個(gè)年齡段的你還看不懂,但如果某天你不那么忙了,要思考思考自己的路、自己的腳步。看看這些是不是你想要的,如果都是你想要的,為什么你看起來不開心?
好!加油,走向你想成為的自己!
二、面試題
謝飛機(jī),小記!,中午吃飽了開始發(fā)呆,怎么就學(xué)不來這些知識(shí)呢,它也不進(jìn)腦子!
「謝飛機(jī)」:喂,面試官大哥,我想問個(gè)問題。
「面試官」:什么?
「謝飛機(jī)」:就是這知識(shí)它不進(jìn)腦子呀!
「面試官」:這....
「謝飛機(jī)」:就是看了忘,忘了看的!
「面試官」:是不是沒有實(shí)踐?只是看了就覺得會(huì)了,收藏了就表示懂了?哪哪都不深入!?
「謝飛機(jī)」:好像是!那有什么辦法?
「面試官」:也沒有太好的辦法,學(xué)習(xí)本身就是一件枯燥的事情。減少碎片化的時(shí)間浪費(fèi),多用在系統(tǒng)化的學(xué)習(xí)上會(huì)更好一些。哪怕你寫寫博客記錄下,驗(yàn)證下也是好的。
三、先動(dòng)手驗(yàn)證垃圾回收
說是垃圾回收,我不引用了它就回收了?什么時(shí)候回收的?咋回收的?
沒有看到實(shí)際的例子,往往就很難讓理科生接受這類知識(shí)。我自己也一樣,最好是讓我看得見。代碼是對數(shù)學(xué)邏輯的具體實(shí)現(xiàn),沒有實(shí)現(xiàn)過程只看答案是沒有意義的。
「測試代碼」
- public class ReferenceCountingGC {
- public Object instance = null;
- private static final int _1MB = 1024 * 1024;
- /**
- * 這個(gè)成員屬性的唯一意義就是占點(diǎn)內(nèi)存, 以便能在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;
- // 假設(shè)在這行發(fā)生GC, objA和objB是否能被回收?
- System.gc();
- }
- }
例子來自于《深入理解Java虛擬機(jī)》中引用計(jì)數(shù)算法章節(jié)。
例子要說明的結(jié)果是,相互引用下卻已經(jīng)置為null的兩個(gè)對象,是否會(huì)被GC回收。如果只是按照引用計(jì)數(shù)器算法來看,那么這兩個(gè)對象的計(jì)數(shù)標(biāo)識(shí)不會(huì)為0,也就不能被回收。但到底有沒有被回收呢?
這里我們先采用 jvm 工具指令,jstat來監(jiān)控。因?yàn)楸O(jiān)控的過程需要我手敲代碼,比較耗時(shí),所以我們在調(diào)用testGC()前,睡眠會(huì) Thread.sleep(55000);。啟動(dòng)代碼后執(zhí)行如下指令。
- 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,第一個(gè)和第二個(gè)幸存區(qū)大小
- S0U、S1U,第一個(gè)和第二個(gè)幸存區(qū)使用大小
- EC、EU,伊甸園的大小和使用
- OC、OU,老年代的大小和使用
- MC、MU,方法區(qū)的大小和使用
- CCSC、CCSU,壓縮類空間大小和使用
- YGC、YGCT,年輕代垃圾回收次數(shù)和耗時(shí)
- FGC、FGCT,老年代垃圾回收次數(shù)和耗時(shí)
- GCT,垃圾回收總耗時(shí)
「注意」:觀察后面三行,S1U = 1288.0、GCT = 0.003,說明已經(jīng)在執(zhí)行垃圾回收。
接下來,我們再換種方式測試。在啟動(dòng)的程序中,加入GC打印參數(shù),觀察GC變化結(jié)果。
- -XX:+PrintGCDetails 打印每次gc的回收情況 程序運(yùn)行結(jié)束后打印堆空間內(nèi)存信息(包含內(nèi)存溢出的情況)
- -XX:+PrintHeapAtGC 打印每次gc前后的內(nèi)存情況
- -XX:+PrintGCTimeStamps 打印每次gc的間隔的時(shí)間戳 full gc為每次對新生代老年代以及整個(gè)空間做統(tǒng)一的回收 系統(tǒng)中應(yīng)該盡量避免
- -XX:+TraceClassLoading 打印類加載情況
- -XX:+PrintClassHistogram 打印每個(gè)類的實(shí)例的內(nèi)存占用情況
- -Xloggc:/Users/xiaofuge/Desktop/logs/log.log 配合上面的使用將上面的日志打印到指定文件
- -XX:HeapDumpOnOutOfMemoryError 發(fā)生內(nèi)存溢出將堆信息轉(zhuǎn)存起來 以便分析
這回就可以把睡眠去掉了,并添加參數(shù) -XX:+PrintGCDetails,如下:
「測試結(jié)果」
- [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
- 從運(yùn)行結(jié)果可以看出內(nèi)存回收日志,F(xiàn)ull GC 進(jìn)行了回收。
- 也可以看出JVM并不是依賴引用計(jì)數(shù)器的方式,判斷對象是否存活。否則他們就不會(huì)被回收啦
「有了這個(gè)例子,我們再接著看看JVM垃圾回收的知識(shí)框架!」
四、JVM 垃圾回收知識(shí)框架
垃圾收集(Garbage Collection,簡稱GC),最早于1960年誕生于麻省理工學(xué)院的Lisp是第一門開始使用內(nèi)存動(dòng)態(tài)分配和垃圾收集技術(shù)的語言。
垃圾收集器主要做的三件事:哪些內(nèi)存需要回收、什么時(shí)候回收、怎么回收。
而從垃圾收集器的誕生到現(xiàn)在有半個(gè)世紀(jì)的發(fā)展,現(xiàn)在的內(nèi)存動(dòng)態(tài)分配和內(nèi)存回收技術(shù)已經(jīng)非常成熟,一切看起來都進(jìn)入了“自動(dòng)化”。但在某些時(shí)候還是需要我們?nèi)ケO(jiān)測在高并發(fā)的場景下,是否有內(nèi)存溢出、泄漏、GC時(shí)間過程等問題。所以在了解和知曉垃圾收集的相關(guān)知識(shí)對于高級程序員的成長就非常重要。
垃圾收集器的核心知識(shí)項(xiàng)主要包括:判斷對象是否存活、垃圾收集算法、各類垃圾收集器以及垃圾回收過程。如下圖;
圖 27-1 垃圾收集器知識(shí)框架
原圖下載鏈接:http://book.bugstack.cn/#s/6jJp2icA
1. 判斷對象已死
1.1 引用計(jì)數(shù)器
- 為每一個(gè)對象添加一個(gè)引用計(jì)數(shù)器,統(tǒng)計(jì)指向該對象的引用次數(shù)。
- 當(dāng)一個(gè)對象有相應(yīng)的引用更新操作時(shí),則對目標(biāo)對象的引用計(jì)數(shù)器進(jìn)行增減。
- 一旦當(dāng)某個(gè)對象的引用計(jì)數(shù)器為0時(shí),則表示此對象已經(jīng)死亡,可以被垃圾回收。
從實(shí)現(xiàn)來看,引用計(jì)數(shù)器法(Reference Counting)雖然占用了一些額外的內(nèi)存空間來進(jìn)行計(jì)數(shù),但是它的實(shí)現(xiàn)方案簡單,判斷效率高,是一個(gè)不錯(cuò)的算法。
也有一些比較出名的引用案例,比如:微軟COM(Component Object Model) 技術(shù)、使用ActionScript 3的FlashPlayer、 Python語言等。
「但是」,在主流的Java虛擬機(jī)中并沒有選用引用技術(shù)算法來管理內(nèi)存,主要是因?yàn)檫@個(gè)簡單的計(jì)數(shù)方式在處理一些相互依賴、循環(huán)引用等就會(huì)非常復(fù)雜??赡軙?huì)存在不再使用但又不能回收的內(nèi)存,造成內(nèi)存泄漏
1.2 可達(dá)性分析法
Java、C#等主流語言的內(nèi)存管理子系統(tǒng),都是通過可達(dá)性分析(Reachability Analysis)算法來判定對象是否存活的。
它的算法思路是通過定義一系列稱為 GC Roots 根對象作為起始節(jié)點(diǎn)集,從這些節(jié)點(diǎn)出發(fā),窮舉該集合引用到的全部對象填充到該集合中(live set)。這個(gè)過程教過標(biāo)記,只標(biāo)記那些存活的對象 好,那么現(xiàn)在未被標(biāo)記的對象就是可以被回收的對象了。
GC Roots 包括;
- 全局性引用,對方法區(qū)的靜態(tài)對象、常量對象的引用
- 執(zhí)行上下文,對 Java方法棧幀中的局部對象引用、對 JNI handles 對象引用
- 已啟動(dòng)且未停止的 Java 線程
「兩大問題」
誤報(bào):已死亡對象被標(biāo)記為存活,垃圾收集不到。多占用一會(huì)內(nèi)存,影響較小。
漏報(bào):引用的對象(正在使用的)沒有被標(biāo)記為存活,被垃圾回收了。那么直接導(dǎo)致的就是JVM奔潰。(STW可以確??蛇_(dá)性分析法的準(zhǔn)確性,避免漏報(bào))
2. 垃圾回收算法
2.1 標(biāo)記-清除算法(mark-sweep)
標(biāo)記-清除算法(mark-sweep)
- 標(biāo)記無引用的死亡對象所占據(jù)的空閑內(nèi)存,并記錄到空閑列表中(free list)。
- 當(dāng)需要?jiǎng)?chuàng)建新對象時(shí),內(nèi)存管理模塊會(huì)從 free list 中尋找空閑內(nèi)存,分配給新建的對象。
- 這種清理方式其實(shí)非常簡單高效,但是也有一個(gè)問題內(nèi)存碎片化太嚴(yán)重了。
- 「Java 虛擬機(jī)的堆中對象」,必須是連續(xù)分布的,所以極端的情況下可能即使總剩余內(nèi)存充足,但尋找連續(xù)內(nèi)存分配效率低,或者嚴(yán)重到無法分配內(nèi)存。重啟湯姆貓!
- 在CMS中有此類算法的使用,GC暫停時(shí)間短,但存在算法缺陷。
2.2 標(biāo)記-復(fù)制算法(mark-copy)
標(biāo)記-復(fù)制算法(mark-copy)
- 從圖上看這回做完垃圾清理后連續(xù)的內(nèi)存空間就大了。
- 這種方式是把內(nèi)存區(qū)域分成兩份,分別用兩個(gè)指針 from 和 to 維護(hù),并且只使用 from 指針指向的內(nèi)存區(qū)域分配內(nèi)存。
- 當(dāng)發(fā)生垃圾回收時(shí),則把存活對象復(fù)制到 to 指針指向的內(nèi)存區(qū)域,并交換 from 與 to 指針。
- 它的好處很明顯,就是解決內(nèi)存碎片化問題。但也帶來了其他問題,堆空間浪費(fèi)了一半。
2.3 標(biāo)記-壓縮算法(mark-compact)
標(biāo)記-壓縮算法(mark-compact)
- 1974年,Edward Lueders 提出了標(biāo)記-壓縮算法,標(biāo)記的過程和標(biāo)記清除算法一樣,但在后續(xù)對象清理步驟中,先把存活對象都向內(nèi)存空間一端移動(dòng),然后在清理掉其他內(nèi)存空間。
- 這種算法能夠解決內(nèi)存碎片化問題,但壓縮算法的性能開銷也不小。
3. 垃圾回收器
3.1 新生代
1.Serial
算法:標(biāo)記-復(fù)制算法
說明:簡單高效的單核機(jī)器,Client模式下默認(rèn)新生代收集器;
2.Parallel ParNew
算法:標(biāo)記-復(fù)制算法
說明:GC線程并行版本,在單CPU場景效果不突出。常用于Client模式下的JVM
3.Parallel Scavenge
算法:標(biāo)記-復(fù)制算法
說明:目標(biāo)在于達(dá)到可控吞吐量(吞吐量=用戶代碼運(yùn)行時(shí)間/(用戶代碼運(yùn)行時(shí)間+垃圾回收時(shí)間));
3.2 老年代
1.Serial Old
算法:標(biāo)記-壓縮算法
說明:性能一般,單線程版本。1.5之前與Parallel Scavenge配合使用;作為CMS的后備預(yù)案。
2.Parallel Old
算法:標(biāo)記-壓縮算法
說明:GC多線程并行,為了替代Serial Old與Parallel Scavenge配合使用。
3.CMS
算法:標(biāo)記-清除算法
說明:對CPU資源敏感、停頓時(shí)間長。標(biāo)記-清除算法,會(huì)產(chǎn)生內(nèi)存碎片,可以通過參數(shù)開啟碎片的合并整理。基本已被G1取代
3.3 G1
算法:標(biāo)記-壓縮算法
說明:適用于多核大內(nèi)存機(jī)器、GC多線程并行執(zhí)行,低停頓、高回收效率。
五、總結(jié)
JVM 的關(guān)于自動(dòng)內(nèi)存管理的知識(shí)眾多,包括本文還沒提到的 HotSpot 實(shí)現(xiàn)算法細(xì)節(jié)的相關(guān)知識(shí),包括:安全節(jié)點(diǎn)、安全區(qū)域、卡表、寫屏障等。每一項(xiàng)內(nèi)容都值得深入學(xué)習(xí)。
如果不僅僅是為了面試背題,最好的方式是實(shí)踐驗(yàn)證學(xué)習(xí)。否則這類知識(shí)就像3分以下的過電影一樣,很難記住它的內(nèi)容。
整個(gè)的內(nèi)容也是小傅哥學(xué)習(xí)整理的一個(gè)過程,后續(xù)還會(huì)不斷的繼續(xù)深挖和分享。感興趣的小伙伴可以一起討論學(xué)習(xí)。