原文發(fā)布于自己的博客平臺【http://www.jetchen.cn/gc/】
我們知道,程序在運行的時候瘟滨,為了提高性能爆哑,大部分?jǐn)?shù)據(jù)都是會加載到內(nèi)存中進行運算的赋续,有些數(shù)據(jù)是需要常駐內(nèi)存中的照藻,但是有些數(shù)據(jù)饺鹃,用過之后便不會再需要了罐农,我們稱這部分?jǐn)?shù)據(jù)為垃圾數(shù)據(jù)条霜。
為了防止內(nèi)存被使用完,我們需要將這些垃圾數(shù)據(jù)進行回收涵亏,即需要將這部分內(nèi)存空間進行釋放宰睡。不同于 C++ 需要自行釋放內(nèi)存的機制,Java 虛擬機(JVM)提供了一種自動回收內(nèi)存的機制气筋,這對于我們開發(fā)人員來說拆内,再友好不過了。
簡介
在做 Java 開發(fā)的過程中裆悄,我們會不斷地創(chuàng)建很多的對象矛纹,這些對象數(shù)據(jù)會占用系統(tǒng)內(nèi)存,如果得不到有效的管理光稼,內(nèi)存的占用會越來越多或南,甚至?xí)霈F(xiàn)內(nèi)存溢出的情況,所以艾君,我們需要進行對內(nèi)存進行合理地釋放采够,這個時候 GC 就派上大用場的。
本文所介紹的垃圾回收(GC)是由 Java 虛擬機(JVM)垃圾回收器提供的一種對內(nèi)存回收的一種機制冰垄,它一般會在內(nèi)存空閑或者內(nèi)存占用過高的時候?qū)δ切]有任何引用的對象不定時地進行回收蹬癌。
不同于 C++ 程序,C++ 是需要開發(fā)人員自己分配內(nèi)存并且進行自行回收內(nèi)存的虹茶,而 Java 程序逝薪,內(nèi)存是托管于 JVM 的,即對象的創(chuàng)建和內(nèi)存的回收都是由 JVM 自行完成的蝴罪,開發(fā)人員是無權(quán)干涉的董济,只能盡量去優(yōu)化。
所以由上述討論我們很容易就會有如下的疑問要门,下文也會依照這幾點疑問來進行深入探討:
JVM 內(nèi)存模型
JVM 內(nèi)存大致分為 線程私有區(qū)域 和 線程共享區(qū)域虏肾,并且其主要由5個區(qū)域組成廓啊,見下圖:
由上圖可以看出,虛擬機棧封豪、本地方法棧和程序計數(shù)器谴轮,這三個區(qū)域是線程私有的。比如棧幀的生命周期是和線程關(guān)聯(lián)的吹埠,即隨線程而生第步,隨線程而死。
虛擬機棧其實就是用來描述 Java 方法執(zhí)行的藻雌,所以每個方法執(zhí)行的時候都會創(chuàng)建一個棧幀雌续,每個棧幀都包含:局部變量斩个、操作數(shù)棧胯杭、動態(tài)鏈接、方法出口受啥,當(dāng)方法執(zhí)行完成之后做个,對應(yīng)的棧幀便會出棧。所以它的內(nèi)存分配是具備確定性的滚局,因此我們并不需要太過關(guān)注包括虛擬機棧在內(nèi)的這幾個線程私有區(qū)域的內(nèi)存使用情況居暖。
相反,另外兩個線程共享的區(qū)域:方法區(qū)和堆內(nèi)存藤肢,則是我們需要重點關(guān)注的對象太闺。因為這兩個區(qū)域主要存放對象、數(shù)組等不具有確定性的數(shù)據(jù)嘁圈,例如創(chuàng)建對象省骂,每個方法運行的過程中創(chuàng)建的對象的數(shù)量是不確定的,即占用的內(nèi)存是不確定的最住,可能不需要創(chuàng)建對象钞澳,也可能會創(chuàng)建很多對象,所以我們需要一套合理的內(nèi)存管理機制來對這兩個區(qū)域進行維護涨缚,因此轧粟,垃圾回收就應(yīng)運而生了,并且這兩個區(qū)域也是垃圾回收器進行垃圾回收的最重要的內(nèi)存區(qū)域脓魏。
我們再來對堆內(nèi)存和方法區(qū)進行一下劃分兰吟,因為 JVM 是采用分代回收的算法,即根據(jù)對象的生命周期進行區(qū)分并進行分代存儲和回收茂翔,其主要分為年輕代混蔼、老年代、持久代檩电,見下圖:
堆內(nèi)存主要由年輕代和老年代組成拄丰,而方法區(qū)主要存儲持久代的數(shù)據(jù)府树,詳細(xì)的細(xì)節(jié)在下文講回收算法的時候會細(xì)說。
注意:從 JDK 1.8 開始料按,永久代已經(jīng)被移除了奄侠,取而代之的是元空間(Meta Space),它和服務(wù)器的內(nèi)存相關(guān)聯(lián)载矿,本文暫不贅述垄潮。
內(nèi)存中的垃圾
程序在運行過程中會創(chuàng)建對象,但是當(dāng)方法執(zhí)行完成或當(dāng)這個對象使用完畢之后闷盔,它便被定義為了“垃圾”弯洗,這時候便需要依靠垃圾回收器去將這塊內(nèi)存區(qū)域清理出來,而對于上述“垃圾”的定義逢勾,我們需要將它量化成計算機語言牡整,即需要設(shè)計一套算法來給垃圾回收器使用,因為畢竟進行垃圾回收的動作是垃圾回收器自動運行并判定的溺拱。
判定一個對象是否是“垃圾”逃贝,即判定一個對象的存活與否,常見的算法有兩種:引用計數(shù)法 和 根搜索算法迫摔。
引用計數(shù)算法(Reference Counting Collector)
一個對象被創(chuàng)建之后沐扳,系統(tǒng)會給這個對象初始化一個引用計數(shù)器,當(dāng)這個對象被引用了句占,則計數(shù)器 +1沪摄,而當(dāng)該引用失效后,計數(shù)器便 -1纱烘,直到計數(shù)器為 0杨拐,意味著該對象不再被使用了,則可以將其進行回收了凹炸。
這種算法其實很好用戏阅,判定比較簡單,效率也很高啤它,但是卻有一個很致命的缺點奕筐,就是它無法避免循環(huán)引用,即兩個對象之間循環(huán)引用的時候变骡,各自的計數(shù)器始終不會變成 0离赫,所以 引用計數(shù)算法 只出現(xiàn)在了早期的 JVM 中,現(xiàn)在基本不再使用了塌碌。
根搜索算法(Tracing Collector)
根搜索算法的中心思想渊胸,就是從某一些指定的根對象(GC Roots)出發(fā),一步步遍歷找到和這個根對象具有引用關(guān)系的對象台妆,然后再從這些對象開始繼續(xù)尋找翎猛,從而形成一個個的引用鏈(其實就和圖論的思想一致)胖翰,然后不在這些引用鏈上面的對象便被標(biāo)識為引用不可達對象,也就是我們說的“垃圾”切厘,這些對象便需要回收掉萨咳。這種算法很好地解決了上面 引用計數(shù)算法 的循環(huán)引用的問題了。
算法的核心思想是很簡單的疫稿,就是標(biāo)記不可達對象培他,然后交由 GC 進行回收,但是有一個點是很重要的遗座,那就是 何為根對象(GC Roots)舀凛?
根對象,一般有如下幾種:
- 虛擬機棧中引用的對象(棧幀中的本地變量表)途蒋;
- 方法區(qū)中常量引用的對象猛遍;
- 方法區(qū)中靜態(tài)屬性引用的對象;
- 本地方法棧中 JNI(Native 方法)引用的對象碎绎;
- 活躍線程螃壤。
但其實抗果,上述算法只是一個算法的中心思想筋帖,實際執(zhí)行過程是比這個復(fù)雜的,另外冤馏,GC 判斷對象是否可達其實看的還是強引用日麸。
1、進行根搜索的時候逮光,是需要暫停所有線程的代箭,即執(zhí)行一次 STW(Stop The World),最主要的目的是防止上述的對象圖在算法運行的過程中有變化從而影響算法的準(zhǔn)確性涕刚。
2嗡综、線程暫停的時間長短,取決于對象的多少杜漠,和堆內(nèi)存的大小無關(guān)极景。
3、 宣告一個對象的“死亡”其實不僅僅通過上述的算法計算驾茴,而是需要經(jīng)歷兩次的標(biāo)記盼樟,本文暫不進行贅述。
回收算法
除了需要上文研究的標(biāo)記“垃圾對象”的算法锈至,我們也需要“清理垃圾”的 回收算法晨缴。
常用的回收算法一般有:標(biāo)記-清除算法、標(biāo)記-整理算法峡捡、復(fù)制算法击碗,以及系統(tǒng)自動進行判定使用的 適應(yīng)性算法筑悴。
標(biāo)記 - 清除算法(Tracing Collector)
標(biāo)記-清除 算法是最基礎(chǔ)的收集算法,它是由 標(biāo)記 和 清除 兩個步驟組成的稍途。
標(biāo)記的過程其實就是上面的 根搜索算法 所標(biāo)記的不可達對象雷猪,當(dāng)所有的待回收的“垃圾對象”標(biāo)記完成之后,便進行第二個步驟:統(tǒng)一清除晰房。
該算法的優(yōu)點是當(dāng)存活對象比較多的時候求摇,性能比較高,因為該算法只需要處理待回收的對象殊者,而不需要處理存活的對象与境。
但是缺點也很明顯,就是在執(zhí)行完 標(biāo)記-整理 之后猖吴,由于將“垃圾對象”回收掉了摔刁,所以原本連續(xù)使用的內(nèi)存塊便會變得不連續(xù),這樣會導(dǎo)致內(nèi)存塊上面會出現(xiàn)很多小單元的內(nèi)存區(qū)域海蔽,這些小單元的內(nèi)存區(qū)域只能夠存放比較小的對象共屈,而比較大的對象是無法直接存儲的。
即原本空閑 1M 的內(nèi)存區(qū)域党窜,有可能會出現(xiàn)無法直接存放 0.9M 大小的對象拗引。
標(biāo)記 - 整理算法(Compacting Collector)
上述的 標(biāo)記-清除 算法會產(chǎn)生內(nèi)存區(qū)域使用的間斷,所以為了將內(nèi)存區(qū)域盡可能地連續(xù)使用幌衣, 標(biāo)記-整理 算法應(yīng)運而生矾削。
標(biāo)記-整理 算法也是由兩步組成,標(biāo)記 和 整理豁护。
第一步的 標(biāo)記 動作也是使用的 根搜索算法哼凯,但是在標(biāo)記完成之后的動作卻和 標(biāo)記-清除算法 天壤之別吴汪,該算法并不會直接清除掉可回收對象 靶累,而是讓所有的對象都向一端移動陆爽,然后將端邊界以外的內(nèi)存全部清理掉是越。
該算法所帶來的最大的優(yōu)勢便是使得內(nèi)存上面不會再有碎片問題藕各,并且新對象的分配只需要通過簡單的指針碰撞便可完成终佛。
復(fù)制算法(Copying Collector)
無論是標(biāo)記-清除算法還是垃圾-整理算法吭历,都會涉及句柄的開銷或是面對碎片化的內(nèi)存回收撬呢,所以吝梅,復(fù)制算法 出現(xiàn)了虱疏。
復(fù)制算法將內(nèi)存區(qū)域均分為了兩塊(記為S0和S1),而每次在創(chuàng)建對象的時候苏携,只使用其中的一塊區(qū)域(例如S0)做瞪,當(dāng)S0使用完之后,便將S0上面存活的對象全部復(fù)制到S1上面去,然后將S0全部清理掉装蓬。
復(fù)制算法的優(yōu)勢是:① 不會產(chǎn)生內(nèi)存碎片著拭;② 標(biāo)記和復(fù)制可以同時進行;③ 復(fù)制時也只需要移動棧頂指針即可牍帚,按順序分配內(nèi)存儡遮,簡單高效;④ 每次只需要回收一塊內(nèi)存區(qū)域即可暗赶,而不用回收整塊內(nèi)存區(qū)域鄙币,所以性能會相對高效一點。
但是缺點也是很明顯的:可用的內(nèi)存減小了一半蹂随,存在內(nèi)存浪費的情況十嘿。
所以 復(fù)制算法 一般會用于對象存活時間比較短的區(qū)域,例如 年輕代岳锁,而存活時間比較長的 老年代 是不適合的绩衷,因為老年代存在大量存活時間長的對象,采用復(fù)制算法的時候會要求復(fù)制的對象較多激率,效率也就急劇下降咳燕,所以老年代一般會使用上文提到的 標(biāo)記-整理算法。
適應(yīng)性算法(Adaptive Collector)
適應(yīng)性算法 其實不是一種單獨的回收算法乒躺,他只是一種智能選擇回收算法的機制招盲,也就是該算法會根據(jù)堆內(nèi)存具體的使用情況而自動選用更適合當(dāng)前情況的回收算法。
分代回收
分代回收 并不是一種垃圾回收算法聪蘸,它是上述各種垃圾回收算法的一個落地應(yīng)用方案宪肖。
因為上述各個算法都有各自的優(yōu)勢,我們在內(nèi)存的使用過程中健爬,有些對象存活時間長,有些對象存活時間短么介,有些對象甚至一直存活著娜遵,所以根據(jù)對象的存活周期,我們將內(nèi)存區(qū)域分為三大塊:年輕代壤短、老年代 和 永久代设拟,并且年輕代也繼續(xù)細(xì)分為:Eden區(qū)、S0 和 S1久脯。
1纳胧、各個內(nèi)存區(qū)域的內(nèi)存大小可以見上文中的內(nèi)存模型圖,當(dāng)然帘撰,我們也可以給 JVM 傳遞參數(shù)來進行調(diào)整跑慕,這些內(nèi)容本文也暫不贅述。
2、 Eden : S0 : S1 的默認(rèn)比例為 8:1:1核行,為什么這么設(shè)計呢牢硅?其實 IBM 有專門的研究表明,年輕代中 98% 的對象都是朝生夕死的芝雪,所以只需要劃分為一個較大的 Eden 區(qū)和兩個較小的 Survivor 區(qū)即可减余,而且這樣做的好處是只有 10% 的 Survivor 區(qū)會被浪費掉,這也是可以接受的惩系。
下面簡單介紹下各個內(nèi)存區(qū)的 GC 過程:
- 對象首次創(chuàng)建進行內(nèi)存分配的時候位岔,首先會放置在 Eden 區(qū),當(dāng) Eden 區(qū)放滿了或者當(dāng)該對象太大無法放進 Eden 區(qū)的時候堡牡,此時會對年輕代(Eden區(qū) 和 S0)進行一次 GC赃承,將幸存下來的對象放置在 S1,然后清空掉 Eden區(qū)和 S0 區(qū)悴侵;(此時年輕代采用的是 復(fù)制算法)
- 在上面第一步中對年輕代進行垃圾回收的時候瞧剖,同時會對幸存的對象進行標(biāo)記,統(tǒng)計每個幸存對象經(jīng)歷的 GC 次數(shù)可免;
- 當(dāng) S1 區(qū)滿了之后抓于,或者年輕代的對象經(jīng)歷過指定次數(shù)的 GC 之后,這部分對象會被放置到老年代之中浇借;
- 當(dāng)老年代也滿了之后捉撮,便會對老年代進行一次 GC;(老年代采用的是 標(biāo)記-整理算法)
垃圾回收器
好了妇垢,上文介紹過了 “垃圾”的識別算法 和 “垃圾”的回收算法巾遭,那么這些算法的執(zhí)行者是誰呢?就是下文介紹的 垃圾回收器(GC) 了闯估。
垃圾回收器的類型
在 Java 語言中灼舍,垃圾回收器按照執(zhí)行機制來進行劃分,主要分為四種類型:
- 串行垃圾回收器(Serial Garbage Collector)涨薪;
- 并行垃圾回收器(Parallel Garbage Collector)骑素;
- 并發(fā)標(biāo)記掃描垃圾回收器(CMS Garbage Collector);
- G1垃圾回收器(G1 Garbage Collector)刚夺。
上述四種垃圾回收器都是有各自的優(yōu)缺點的献丑,我們可以通過向 JVM 傳遞參數(shù)來指定其中一款垃圾回收器。
1侠姑、串行垃圾回收器(Serial Garbage Collector)
串行垃圾回收器會暫停所有的應(yīng)用程序線程创橄,并采用單獨的的線程進行 GC。
適用于單 CPU莽红、并且對應(yīng)用程序的暫停時間要求不高的情況妥畏,所以不太適合當(dāng)前的生產(chǎn)環(huán)境。
2、并行垃圾回收器(Parallel Garbage Collector)
并行垃圾回收器是 JVM 默認(rèn)的垃圾回收器咖熟,相較于串行垃圾回收器而言性能稍有提升圃酵,它也是需要暫停所有的應(yīng)用程序線程的,但是區(qū)別是它會使用多線程進行 GC馍管。
所以并行垃圾回收器適用于多 CPU 的服務(wù)器郭赐、并且能接受短暫的應(yīng)用暫停的程序。
3确沸、并發(fā)標(biāo)記掃描垃圾回收器(CMS Garbage Collector)
CMS 回收器也是一種并行的垃圾回收器捌锭,它會采用多線程來進行掃描堆內(nèi)存,標(biāo)記需要清理的對象并將這些對象清理掉罗捎。
但是 CMS 它需要更多的 CPU 來保證程序的吞吐量观谦,并且它保證了最短的回收停頓時間,所以桨菜,在服務(wù)器允許的情況下豁状,為了達到更到的性能,我們應(yīng)該使用 CMS 來代替默認(rèn)的 并行垃圾回收器倒得。
4泻红、G1 垃圾回收器(G1 Garbage Collector)
G1 垃圾回收器是在 JDK1.7 中才正式引入的一款垃圾回收器,“科技在進步霞掺,所以一般越是先進的技術(shù)一般會更好用并且會替代陳舊的技術(shù)”谊路,好了,玩笑歸玩笑菩彬,但是 G1 的引入缠劝,目的就是為了取代 CMS 的。
不要被上面 G1 的示意圖誤導(dǎo)骗灶, G1 并沒有將內(nèi)存進行物理劃分惨恭,它只是將堆內(nèi)存劃分為一個個的 Region,但是也是屬于分代垃圾回收器矿卑,G1 仍然會區(qū)分年輕代和老年代喉恋,并且年輕代仍然會有 Eden 區(qū)和 Survivor 區(qū)。
這么做的目的是保證 G1 回收器在有限的時間內(nèi)可以獲得盡可能高的回收效率母廷。
HotSpot 虛擬機(HotSpot VM)提供的幾種垃圾收集器
HotSpot VM 提供了 7 種垃圾收集器,分別為:
- Serial
- PraNew
- Parallel Scavenge
- Serial Old
- Parallel Old
- CMS
- G1
其中糊肤,1琴昆、2、3 種適合年輕代內(nèi)存區(qū)的垃圾回收馆揉,4业舍、5、6種適合老年代內(nèi)存區(qū)的垃圾回收,并且它們之間是兩兩組合來進行使用的舷暮,詳見下圖:
垃圾回收的時機
垃圾回收分為兩種态罪,F(xiàn)ull GC 和 Scavenge GC。
Full GC 發(fā)生在整個堆內(nèi)存中下面,而 Scavenge GC 僅僅發(fā)生在年輕代的 Eden 區(qū)复颈,所以我們應(yīng)該盡可能地減少 Full GC 的次數(shù),當(dāng)然沥割,對于 JVM 的調(diào)優(yōu)耗啦,很多情況下也是在想辦法對 Full GC 進行調(diào)優(yōu)。
因為 GC 是可能會對應(yīng)用程序造成影響的机杜,所以觸發(fā) GC 也是有一定的條件的帜讲,例如:
- 當(dāng)應(yīng)用程序空閑時,GC 有可能會被調(diào)用椒拗,因為 GC 運行線程的優(yōu)先級是相對較低的似将,所以當(dāng)線程忙的時候,它是不會運行的蚀苛,當(dāng)然在验,內(nèi)存不足的情況除外;
- 堆內(nèi)存不足的時候枉阵,GC 會被調(diào)用译红。例如創(chuàng)建對象的時候,若此時內(nèi)存不足兴溜,則會觸發(fā) GC 用來給這個對象分配合適的內(nèi)存侦厚,當(dāng)進行完一次 GC 之后內(nèi)存還是不足,則會繼續(xù)進行第二次 GC拙徽,若第二次 GC 之后內(nèi)存還是不足刨沦,則一般會提示 “out of memory”異常;
小 Tip:
System.gc()
方法會顯示觸發(fā) Full GC膘怕,但是它只是對 JVM 的一個 GC 請求想诅,至于何時觸發(fā),還是由 JVM 自行判斷的岛心。
GC 的調(diào)用開銷是比較大的来破,所以我們需要有針對性地進行調(diào)優(yōu),一般有如下方案:
- 不要顯式調(diào)用
System.gc()
忘古。此函數(shù)雖然是建議 JVM 進行 GC徘禁,但很多情況下它會觸發(fā) GC,從而增加 GC 的頻率髓堪; - 盡量減少臨時對象的使用送朱。在方法結(jié)束后娘荡,臨時對象便成為了垃圾,所以減少臨時變量的使用就相當(dāng)于減少了垃圾的產(chǎn)生驶沼,從而減少了GC的次數(shù)炮沐;
- 對象不用時最好顯式置為 Null。一般而言回怜,為 Null 的對象都會被作為垃圾處理大年,所以將不用的對象顯式地設(shè)為 Null 有利于 GC 收集器對垃圾的判定;
- 盡量使用 StringBuilder 來代替 String 的字符串累加鹉戚。因為 String 的底層是 final 類型的數(shù)組鲜戒,所以 String 的增加其實是建了一個新的 String,從而產(chǎn)生了過多的垃圾抹凳;
- 允許的情況下盡量使用基本類型(如 int)來替代 Integer 對象遏餐。因為基本類型變量比相應(yīng)的對象占用的內(nèi)存資源會少得多;
- 合理使用靜態(tài)對象變量赢底。因為靜態(tài)變量屬于全局變量失都,不會被 GC 回收;
其它
JVM 的 GC幸冻,它就像能看到也能感受到的真實存在的事物粹庞,但是當(dāng)我們?nèi)ド焓謮蛩臅r候,此時它又是虛無縹緲般的存在洽损,處理它的時候還需要格外地謹(jǐn)慎庞溜。
因為它的不確定性,所以我們不應(yīng)該去假定 GC 觸發(fā)的時間碑定,也不要去使用類似 System.gc()
這樣顯示調(diào)用 GC 的方法流码,這些都是得不償失的。
最需要注意的是我們的編程習(xí)慣和編程態(tài)度延刘,良好的編程習(xí)慣能夠幫助我們規(guī)避掉很多內(nèi)存方面的問題漫试,包括但不僅限于內(nèi)存泄露等。
最后碘赖,由于垃圾回收器眾多驾荣,在特定的情況下,我們是可以指定使用垃圾回收器的類型的普泡,例如使用:-X:+UseG1GC
來指定使用 G1 垃圾回收器播掷。