第一部分:面試題
本次分享我們將嘗試回答以下問題:
- GC 是什么? 為什么要有 GC?
- 簡單說一下java的垃圾回收機(jī)制柴灯。
- JVM的常見垃圾回收算法有哪些泡嘴?
- 為什么要使用分代回收機(jī)制蔓纠?
- 如何判斷一個對象是否存活?
- 如果對象的引用被置為 null溃槐,垃圾收集器是否會立即釋放對象占用的內(nèi)存?
第二部分:深入原理
好帅矗,讓我們開始吧偎肃。還是那句話,如果時間不夠可以直接拉到最后看答案浑此。
java垃圾回收的知識點(diǎn)雖然看起來難累颂,但知識點(diǎn)非常集中,而且很好理解凛俱。不信紊馏?不信就往下看吧。
1. 所謂GC
GC就是垃圾收集的意思(Gabage Collection)最冰。
我們在開發(fā)中會創(chuàng)建很多對象瘦棋,這些對象一股腦的都扔進(jìn)了堆里(還記得jvm內(nèi)存模型嗎稀火?不記得的話翻翻前面的文章)暖哨,如果這些對象只增加不減少,那么堆空間很快就會被耗盡凰狞。所以我們需要把一些沒用的對象清理掉篇裁。
2.對象已死嗎
垃圾回收,就是要把那些不再使用的對象找出來然后清理掉赡若,釋放其占用的內(nèi)存空間达布。
判斷一個對象是否還在使用,用咱們java圈子的行話講逾冬,就是判斷對象是否死亡(反之就是存活)黍聂。
在java中判斷對象死亡有兩種方式:
- 引用計(jì)數(shù)法
- 可達(dá)性分析法
下面我們詳細(xì)講講
2.1 引用計(jì)數(shù)法
引用計(jì)數(shù)法的思想十分樸素,它的做法是給對象添加一個引用計(jì)數(shù)器身腻,每當(dāng)有一個地方引用該對象产还,這個計(jì)數(shù)器就加1。當(dāng)引用失效時嘀趟,計(jì)數(shù)器就減1脐区。如果計(jì)數(shù)器為0了,說明該對象不再被引用她按,成為死亡對象牛隅。
不過這種算法有一個致命缺點(diǎn),就是無法處理對象相互引用的情況酌泰。
你看媒佣,假如有A、B兩個對象陵刹,它們互相引用默伍,那么對象中的引用計(jì)數(shù)器會始終大于0。
所以這種算法已經(jīng)沒人用了。
2.2 可達(dá)性分析法
2.2.1 什么是可達(dá)性
可達(dá)性分析法就是目前的主流算法巡验,也是java正在使用的算法际插。
它的做法是,通過一系列被稱為“GC Roots”的對象作為起點(diǎn)显设,從這些起點(diǎn)開始往下搜索框弛,搜索所走過的路徑稱為引用鏈(Reference Chain)。當(dāng)一個對象沒有和任何引用鏈相連捕捂,即稱為該對象不可達(dá)(圖論的說法)瑟枫,認(rèn)為該對象死亡。
來看下面這張圖:
上圖中A指攒、B慷妙、C都跟GC Roots有直接或間接的引用關(guān)系,所以是存活對象允悦。而D膝擂、E、F雖然相互之間有引用隙弛,但是和GC Roots并無引用關(guān)系架馋,所以是死亡對象。
2.2.2 哪些對象可作為GC Roots
有四類對象可作為可達(dá)性分析的GC Roots
- 棧(棧幀中的本地變量表)中引用的對象
- 方法區(qū)中類靜態(tài)屬性引用的對象
- 方法區(qū)中常量引用的對象
- 本地方法棧中JNI引用的對象
總而言之全闷,GC Roots是所有Java線程中處于活躍狀態(tài)的棧幀,靜態(tài)引用等指向GC堆里的對象的引用叉寂。換句話說,就是當(dāng)前所有正在被調(diào)用的方法的引用類型的參數(shù)/局部變量/臨時值总珠。
2.3 所謂引用
對象是否死亡屏鳍,關(guān)鍵就在于引用。在java中局服,引用其實(shí)有四種:強(qiáng)引用钓瞭、軟引用、弱引用腌逢、虛引用降淮。
-
強(qiáng)引用
強(qiáng)引用就是我們?nèi)粘i_發(fā)中最常見的引用,例如
String str = new String("hello");
只要強(qiáng)引用還在搏讶,對象就不會被回收佳鳖。
-
軟引用
軟引用需要專門聲明,例如
SoftReference<String> str = new SoftReference<String>("hello");
被軟引用關(guān)聯(lián)的對象在內(nèi)存不足時會被回收媒惕。
這個特性特別適合用來做緩存系吩。
-
弱引用
弱引用也需要專門聲明,例如
WeakReference<String> str = new WeakReference<String>("hello");
被弱引用關(guān)聯(lián)的對象每次GC時都會被回收妒蔚。
弱引用最常見的用途是實(shí)現(xiàn)可自動清理的集合或者隊(duì)列穿挨。
-
虛引用
虛引用是最弱的引用月弛,需要用PhantomReference來聲明,例如
PhantomReference<String> phantom = new PhantomReference<>(new String("hello"), new ReferenceQueue<>());
它完全不會影響對象的生存時間科盛,唯一的作用是在對象被回收時發(fā)一個系統(tǒng)通知帽衙。
2.4 起死回生
對象在被判定為死亡后,并不會立刻被回收贞绵,而是要經(jīng)過一個過程才會被回收厉萝。在這個回收過程中,死亡對象還有可能活過來榨崩,是不是很神奇谴垫?
來看圖:
上圖是對象被回收的過程。一個對象要被回收母蛛,至少要經(jīng)過兩次標(biāo)記翩剪。
如果對象在第二次標(biāo)記之前重新連接上GC Roots,那么它將在第二次標(biāo)記中被移出回收隊(duì)列彩郊,從而復(fù)活前弯。
還有一點(diǎn)需要注意的是,F(xiàn)inalizer線程是一個由虛擬機(jī)自動建立焦辅,且低優(yōu)先級的線程博杖。該線程觸發(fā)對象的finalize()方法之后椿胯,并不會阻塞等待方法執(zhí)行結(jié)束筷登。這樣做是為了防止回收隊(duì)列被阻塞。
finalize()是Object中的方法哩盲,當(dāng)垃圾回收器將要回收對象所占內(nèi)存之前被調(diào)用的方法前方。有些教材推薦用該方法來做“關(guān)閉外部資源”之類的工作,但是實(shí)際上該方法運(yùn)行代價高昂廉油,且不確定性很大惠险,所以并不推薦使用。真要關(guān)閉外部資源抒线,還不如用try-finally來處理班巩。
3.方法區(qū)的回收
方法區(qū)不在堆內(nèi),會被垃圾回收嗎嘶炭?
在jdk1.7中抱慌,方法區(qū)在永久代,而永久代本身就是垃圾回收概念下的產(chǎn)物眨猎,full gc時就會對方法區(qū)回收抑进。
到了jdk1.8,雖然永久代被取消睡陪,但是新增了MaxMetaspaceSize參數(shù)寺渗,對于將死的類及類加載器的垃圾回收將在元數(shù)據(jù)使用達(dá)到“MaxMetaspaceSize”參數(shù)的設(shè)定值時進(jìn)行匿情。
所以,方法區(qū)會被回收信殊。
4.垃圾回收算法
這一節(jié)我們來看下流行的垃圾回收算法炬称,只說思想,不涉及實(shí)現(xiàn)細(xì)節(jié)涡拘。
我們需要了解的垃圾回收算法有以下幾種:
- 標(biāo)記-清除算法
- 復(fù)制算法
- 標(biāo)記-整理算法
- 分代回收算法
咱們一個個來看下转砖。
4.1 標(biāo)記-清除算法
標(biāo)記-清除算是最基本的回收算法了。它的思想就是先標(biāo)記鲸伴,再清除府蔗。標(biāo)記過程如2.4節(jié)所述,有兩次標(biāo)記汞窗。
它的主要缺點(diǎn)有兩個:
- 效率不高
- 會產(chǎn)生大量內(nèi)存碎片
內(nèi)存碎片是指內(nèi)存的空間比較零碎姓赤,缺少大段的連續(xù)空間。這樣假如突然來了一個大對象仲吏,會找不到足夠大的連續(xù)空間來存放不铆,于是不得不再觸發(fā)一次gc。
4.2 復(fù)制算法
復(fù)制算法的思想是裹唆,把內(nèi)存分成兩塊誓斥,假設(shè)分成A、B兩個區(qū)域吧许帐。
每次對象過來之后劳坑,都放到A區(qū)域里,當(dāng)A區(qū)域滿了之后成畦,把存活的對象復(fù)制到B區(qū)域距芬,然后清空A區(qū)域。
接下來的對象就全部放到B區(qū)域循帐,等B區(qū)域滿了框仔,就把存活對象復(fù)制到A區(qū)域,然后清空B區(qū)域拄养。
就這樣來回倒騰离斩,完成垃圾回收。
優(yōu)點(diǎn)是不會有空間碎片瘪匿,缺點(diǎn)是每次只用得到一半內(nèi)存跛梗。
缺點(diǎn)是在對象存活率較高的場景下(比如老年代那樣的環(huán)境),需要復(fù)制的東西太多柿顶,效率會下降茄袖。
4.3 標(biāo)記-整理算法
標(biāo)記-整理算法中的“標(biāo)記”階段和“標(biāo)記-清理”中的標(biāo)記一樣。不同的是嘁锯,死亡對象并不會直接清理宪祥,而是把他們在內(nèi)存中都移動到一起聂薪,然后一起清理。
4.4 分代收集算法
分代收集算法其實(shí)沒什么新東西蝗羊,只是把對象按存活率分塊藏澳,然后選用合適的收集算法。
java中使用的就是分代收集算法耀找。
存活率低的對象放在一起翔悠,稱為年輕代,使用復(fù)制算法來收集野芒。
存活率高的對象放在一起蓄愁,稱為老年代,使用標(biāo)記-清除或者標(biāo)記-整理算法狞悲。
5. HotSpot的枚舉GC Roots
前面我們說到了對象的可達(dá)性分析需要從GC Roots開始計(jì)算引用鏈撮抓。
然而可作為GC Roots的對象非常多,一個個來計(jì)算將非常耗時摇锋。
而且在進(jìn)行這項(xiàng)工作時丹拯,虛擬機(jī)必須停下來,就像時間停止那樣(Sun稱之為Stop The World荸恕,哈哈乖酬,是不是很酷),以此保證分析結(jié)果的準(zhǔn)確性融求。
我們的程序咬像,特別是網(wǎng)站應(yīng)用,基本是上是一刻不停的在運(yùn)行的双肤。如果出現(xiàn)長時間的停止施掏,基本上是不可接受的。為了解決這個問題茅糜,各個虛擬機(jī)都采取了一些措施,盡量減少停頓時間(是的素挽,只能減少蔑赘,停頓是不可能消除的)。
我們來看看現(xiàn)在最流行的Hotspot虛擬機(jī)是怎么處理的预明。(還記得啥是HotSpot不缩赛?翻翻前幾篇文章)
5.1 OopMap
在HotSpot中,虛擬機(jī)把對象內(nèi)的什么偏移量上是什么類型的數(shù)據(jù)的信息存在到一個叫做“OopMap”的數(shù)據(jù)結(jié)構(gòu)中撰糠。這樣在計(jì)算引用鏈時直接查OopMap即可酥馍,不用到整個內(nèi)存中去挨個找了,由此提高了分析速度阅酪。
5.2 安全點(diǎn)
然而旨袒,程序中的引用關(guān)系時時刻刻都在變化汁针,如果每次變化都要記錄到OopMap中,也是一項(xiàng)很大的負(fù)擔(dān)砚尽。所以施无,只有在程序執(zhí)行到了特定的位置,才會去記錄到OopMap中必孤。
這個“特定的位置”猾骡,就叫安全點(diǎn)。
這里面還有個問題敷搪,就是如何保證在GC發(fā)生時兴想,讓所有的線程正好到達(dá)安全點(diǎn)。
有兩種方式:
-
搶先式中斷(已經(jīng)沒人用了)
搶先式中斷的思路是赡勘,先把所有線程中斷襟企,如果有線程沒有跑到安全點(diǎn)上,就恢復(fù)該線程狮含,讓它跑到安全點(diǎn)顽悼。
-
主動式中斷
主動式中斷的做法是,設(shè)置一個中斷標(biāo)志几迄,這個標(biāo)志和安全點(diǎn)是重合的蔚龙。讓各個線程去輪詢這個標(biāo)志,發(fā)現(xiàn)需要中斷時映胁,線程就自己中斷掛起木羹。
5.3 安全區(qū)域
雖然安全點(diǎn)已經(jīng)完美解決了如何保證在GC發(fā)生時,讓所有的線程正好到達(dá)安全點(diǎn)的問題解孙。
但是有一些情況下坑填,線程失去了行為能力,比如線程處于sleep或者blocked狀態(tài)弛姜。這個時候線程無法去響應(yīng)JVM的中斷請求脐瑰,而JVM顯然也不肯能一直等待某幾個線程。該怎么辦呢廷臼?
這種情況就需要“安全區(qū)域”來解決苍在。
安全區(qū)域是指在一段代碼片段中,引用關(guān)系不會發(fā)生變化荠商,這個區(qū)域中任意地方開始GC都是安全的寂恬。
6.垃圾收集器
前面咱們說的都是垃圾收集的方法和思路,垃圾收集器則是具體的實(shí)現(xiàn)莱没。
先來看下hotSpot中垃圾收集器的總圖(到j(luò)dk1.8)
6.1 并行和并發(fā)
在開始講解之前初肉,我們先了解一下什么是并行和并發(fā)。
并行:垃圾收集器是多線程同時工作的饰躲,但是用戶線程仍然處于等待狀態(tài)牙咏。
并發(fā):用戶線程和垃圾收集器線程同時執(zhí)行(也有可能是交替執(zhí)行)臼隔。
下面咱們說說幾個常用的使用方案
6.1 jdk1.8默認(rèn)垃圾收集器
查看當(dāng)前使用的垃圾收集器可以使用以下命令:
~ java -XX:+PrintCommandLineFlags -version
然后會看到以下內(nèi)容:
-XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
java version "1.8.0_151"
Java(TM) SE Runtime Environment (build 1.8.0_151-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.151-b12, mixed mode)
可見jdk1.8默認(rèn)工作在Server模式下,默認(rèn)使用ParallelGC垃圾收集器眠寿。
如果要看更詳細(xì)的信息躬翁,還可以使用以下命令:
java -XX:+PrintFlagsFinal -version | grep GC
這個命令打印的內(nèi)容有點(diǎn)多,我們主要找值為true的信息盯拱。默認(rèn)情況會有以下兩行:
bool UseParallelGC := true
bool UseParallelOldGC = true
6.1.1 Parallel Scavenge收集器
從上面的總圖能看到盒发,這是一個工作在年輕代的收集器,使用復(fù)制算法狡逢,是一個并行的多線程收集器宁舰。
它的目標(biāo)是達(dá)到一個可控制的吞吐量。所謂吞吐量就是CPU用于運(yùn)行用戶代碼的時間與CPU總消耗時間的比值奢浑。比如虛擬機(jī)總共運(yùn)行了100分鐘蛮艰,其中垃圾收集花了1分鐘,那吞吐量就是99%雀彼。
6.1.2 Parallel Old收集器
Parallel Old是一個工作在老年代的收集器壤蚜,使用“標(biāo)記-整理”算法。也是一個關(guān)注吞吐量的垃圾收集器徊哑。
6.2 web應(yīng)用垃圾收集器方案
ParallelGC組合重視的是吞吐量袜刷,非常適合在后臺運(yùn)算而不需要太多交互的場景。
對于需要大量交互的應(yīng)用莺丑,比如web應(yīng)用著蟹,則需要更短的停頓時間。
所以大多數(shù)web應(yīng)用使用的是ParNew+CMS收集器方案梢莽。
6.2.1 ParNew收集器
parNew也是一個工作在年輕代的收集器萧豆,也使用復(fù)制算法,也是一個并行的多線程收集器昏名。
為什么我要使用這么多“也”……
好吧涮雷,parNew看起來和Parallel Scavenge一模一樣,但其實(shí)他們還是有區(qū)別的葡粒。
parNew是一個重視停頓時間收集器份殿。
不過它最大的特點(diǎn)是:可以和CMS收集器組隊(duì)工作。
Parallel Scavenge就不行…...
6.2.2 CMS收集器
CMS是一款十分優(yōu)秀的老年代垃圾收集器嗽交,響應(yīng)速度快、停頓時間短颂斜,是現(xiàn)在大多數(shù)互聯(lián)網(wǎng)公司的選擇夫壁,大家要好好掌握。
CMS使用“標(biāo)記-清除”算法沃疮,分為4個步驟:
- 初始標(biāo)記(STW)
- 并發(fā)標(biāo)記
- 重新標(biāo)記(STW)
- 并發(fā)清除
其中盒让,初始標(biāo)記很快梅肤,只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對象。
并發(fā)標(biāo)記和重新標(biāo)記要Stop The World邑茄,并發(fā)標(biāo)記就是在標(biāo)記死亡對象姨蝴,重新標(biāo)記是為了修正并發(fā)標(biāo)記期間發(fā)生變動的那部分對象。
從耗時來看肺缕,并發(fā)標(biāo)記>重新標(biāo)記>初始標(biāo)記左医。
并發(fā)清除和并發(fā)標(biāo)記耗時最長,但收集器線程是和用戶線程一起并發(fā)執(zhí)行的同木,所以沒有停頓浮梢。
CMS固然優(yōu)秀,但也有一些缺點(diǎn):
-
耗CPU資源
收集器線程和用戶線程并發(fā)工作彤路,所以收集時會搶占CPU資源
-
無法處理浮動垃圾
浮動垃圾是指在標(biāo)記過程之后出現(xiàn)的垃圾秕硝。這部分垃圾在本次回收中無法處理,只能等下次洲尊。
-
產(chǎn)生碎片空間
使用“標(biāo)記-清除”算法就會有這個問題曲聂。不過可以通過參數(shù)設(shè)置開啟碎片整理,比如3次回收后就來一次帶碎片整理的回收后室。
6.3 G1收集器
G1收集器是目前最新的垃圾收集器他炊,到j(luò)dk1.7時達(dá)到可商用程度。
G1收集器可以同時hold住年輕代和老年代姆吭,不需要和別的收集器搭配使用榛做。
G1收集器使用的也是分代算法,它的思路是内狸,把內(nèi)存空間分成一個個小格子检眯,每個格子稱為一個Region。如下圖:
優(yōu)先回收價值大的Region昆淡。
年輕代使用并發(fā)復(fù)制算法锰瘸,有STW。
老年代回收步驟大致可以分為以下幾個:
- 初始標(biāo)記(STW)
- 并發(fā)標(biāo)記
- 最終標(biāo)記(STW)
- 篩選回收(STW)
目前JDK1.9已經(jīng)默認(rèn)使用G1收集器昂灵,但是在JDK1.8版本中G1收集器似乎還有不少問題避凝,使用的還不多。
7.內(nèi)存分配策略
終于要放出這張圖了:
其實(shí)我在一開頭就像放這張圖眨补,但是想著先講點(diǎn)前置知識管削,沒想到這一講,就叫講到這了…...
7.1 年輕代的策略
在年輕代分為三個區(qū)域撑螺,Eden區(qū)含思、Survivor1區(qū)、Survivor2區(qū)。有時候Survivor1區(qū)含潘、Survivor2區(qū)又叫from區(qū)和to區(qū)饲做。
對象優(yōu)先分配到Eden區(qū)。Eden區(qū)要滿的時候遏弱,會有一次復(fù)制回收盆均,把存活的對象放到Survivor1區(qū)。
等Eden區(qū)再次要滿的時候漱逸,又會有一次復(fù)制回收泪姨,把Eden區(qū)和Survivor1區(qū)的存活對象放到Survivor2區(qū)。
然后如此循環(huán)虹脯。
7.2 大對象的策略
虛擬機(jī)提供了一個-XX:PretenureSizeThreshold參數(shù)驴娃,大于這個參數(shù)的對象會直接進(jìn)入老年代,防止年輕代發(fā)生大量內(nèi)存復(fù)制循集。
7.3 晉升策略
年輕代的對象沒熬過一次Minor GC唇敞,年齡就加一歲。默認(rèn)15歲時咒彤,就會進(jìn)入老年代疆柔。
不過這個條件并非絕對,如果Survivor中相同年齡的對象總和大于Survivor空間的一半镶柱,那么年齡大于等于該年齡的對象可以直接晉升到老年代旷档。
7.4 空間分配擔(dān)保
年輕代在Minor GC后會有對象進(jìn)入老年代,在極端情況下歇拆,年輕代所有對象都存活并進(jìn)入老年代鞋屈。
所以在MinorGC之前,虛擬機(jī)會檢查老年代的連續(xù)內(nèi)存空間是否大于年輕代所有對象總和故觅。
如果空間不夠厂庇,那么這次MinorGC是有風(fēng)險的。
如果允許冒險输吏,Minor GC會直接執(zhí)行权旷,如果失敗,會再發(fā)起一次full GC贯溅。
如果不允許冒險拄氯,則先執(zhí)行一次full GC,再進(jìn)行Minor GC它浅。
第三部分:面試題答案
-
GC 是什么? 為什么要有 GC?
GC就是垃圾回收译柏,釋放掉沒用的對象占用的空間,保證內(nèi)存空間不被迅速耗盡姐霍。
-
簡單說一下java的垃圾回收機(jī)制艇纺。
java采用分代回收,分為年輕代邮弹、老年代黔衡、永久代。年輕代又分為E區(qū)腌乡、S1區(qū)盟劫、S2區(qū)。
到j(luò)dk1.8与纽,永久代被元空間取代了侣签。
年輕代都使用復(fù)制算法,老年代的收集算法看具體用什么收集器急迂。默認(rèn)是PS收集器影所,采用標(biāo)記-整理算法。
-
JVM的常見垃圾回收算法有哪些僚碎?
復(fù)制猴娩、標(biāo)記清除、標(biāo)記整理勺阐、分代回收
-
為什么要使用分代回收機(jī)制卷中?
因?yàn)闆]有一種算法能適用所有場合。在對象存活率低的場景下渊抽,復(fù)制算法最合適蟆豫。
對象存活率高時,標(biāo)記清除或者標(biāo)記整理算法最合適懒闷。
所以才需要分代來處理十减。
-
如何判斷一個對象是否存活?
現(xiàn)在主流使用的都是可達(dá)性分析法。從GC Roots對象計(jì)算引用鏈愤估,能鏈上的就是存活的帮辟。
-
如果對象的引用被置為 null,垃圾收集器是否會立即釋放對象占用的內(nèi)存?
不會灵疮。對象回收需要一個過程织阅,這個過程中對象還能復(fù)活。而且垃圾回收具有不確定性震捣,指不定什么時候開始回收荔棉。
引用文獻(xiàn)
《深入理解Java虛擬機(jī)》
系列文章總目錄:https://mp.weixin.qq.com/s/56JgXLArTAEDj1f3y4arLA