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