深入理解JVM學(xué)習(xí)筆記-垃圾收集器與內(nèi)存分配策略

垃圾回收需要完成三件事

(1)那些內(nèi)存需要回收。
(2)什么時候回收。
(3)如何回收。
上篇文章深入理解JVM學(xué)習(xí)筆記-Java內(nèi)存區(qū)域與內(nèi)存溢出異常中介紹了Java內(nèi)存運(yùn)行時區(qū)域的的各個部分,其中程序計數(shù)器、虛擬機(jī)棧、本地方法棧3三個區(qū)域隨線程而生,隨線程而滅,棧中的棧幀隨著方法的進(jìn)入和退出而有條不紊的執(zhí)行著出棧和入棧的操作,每個棧幀中分配多少內(nèi)存基本上是類結(jié)構(gòu)確定下來時已知的,因此這幾個區(qū)域的內(nèi)存分配和回收都具備確定性,在這幾個區(qū)域內(nèi)不需要過多的考慮回收的問題,因為方法結(jié)束或者線程結(jié)束時,內(nèi)存自然就跟著回收了,而Java堆和方法區(qū)不一樣,這部分內(nèi)存分配和回收都是動態(tài),垃圾收集器所關(guān)注的就是這部分內(nèi)存。

如何判斷對象已死?

引用計數(shù)算法:給對象中添加一個引用計數(shù)器,每當(dāng)有一個地方引用它時,計數(shù)器值就加1,當(dāng)解除引用時,計數(shù)器值就減1,引用計數(shù)器為0的對象就可以認(rèn)為對象已死,可以進(jìn)行內(nèi)存回收。但是引用計數(shù)算反很難解決對象之間的互相循環(huán)引用問題。
可達(dá)性分析算法:以GC Roots對象為起始點進(jìn)行搜索,如果有對象不可達(dá),那么該對象就是垃圾對象,即使兩個對象互相有引用關(guān)系,只要GC Roots是不可達(dá)的,那么這兩個對象是可回收對象。
GC Roots對象包括以下幾種:
(1)虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象。
(2)方法區(qū)中類靜態(tài)屬性引用的對象。
(3)方法區(qū)中常量引用的對象。
(4)本地方法棧中JNI(一般說的是Native方法)引用的對象。

Java四種引用

強(qiáng)引用(Strong Reference):就是指在程序代碼之中普遍存在的,類似“Object Object = new Object()”這類的引用,只要強(qiáng)引用還存在,垃圾收集器永遠(yuǎn)不會回收掉被引用的對象。
軟引用(Soft Reference):是用來描述一些還有用但并非必須的獨享,對于軟引用關(guān)聯(lián)的對象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會把這些對象進(jìn)行垃圾回收。如果回收之后還沒有足夠的內(nèi)存,才會拋出內(nèi)存溢出異常。
弱引用(Weak Reference):也是用來描述非必須對象的,但是它的強(qiáng)度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集發(fā)生之前,當(dāng)垃圾收集器進(jìn)行垃圾時,無論內(nèi)存是否足夠,都會回收掉只被弱引用關(guān)聯(lián)的對象。
虛引用(Phantom Reference):是最弱的引用關(guān)系,一個對象是否有虛引用的存在,完全不對對其生存時間構(gòu)成影響。

對象生存還是死亡

可達(dá)性分析算法中不可達(dá)的對象,也并非非死不可,要真正宣告對象死亡,至少要經(jīng)理兩次標(biāo)記過程:如果對象在進(jìn)行可達(dá)性分析后,發(fā)現(xiàn)沒有與GC Roots相連接的引用鏈,那么對象將會進(jìn)行第一次標(biāo)記。第二次標(biāo)記就是判斷對象有沒有實現(xiàn)finalize()方法,如果沒有實現(xiàn)就直接判斷該對象可回收;如果實現(xiàn)了就會先放在一個隊列中,并由虛擬機(jī)建立的一個低優(yōu)先級的線程去執(zhí)行它,隨后就會進(jìn)行第二次的小規(guī)模標(biāo)記,在這次被標(biāo)記的對象就會真正的被回收了。任何一個對象的finalize()方法都只會被系統(tǒng)自動調(diào)用一次,如果對象面臨下一次回收,它的finalize()方法不會被再次執(zhí)行,因此,如果再次使用該方法,對象的自救行動是不會成功的了(JVM不建議在finalize()拯救對象)。

回收方法區(qū)

方法區(qū)內(nèi)存在虛擬機(jī)中的永久代,Java虛擬機(jī)規(guī)范說過可以不要求虛擬機(jī)在方法區(qū)實現(xiàn)垃圾收集,而且方法區(qū)垃圾收集性價比一般比較低(永久代)。永久代垃圾收集主要回收兩部分內(nèi)容:廢棄常量和無用類,回收廢棄常量與回收J(rèn)ava堆中對象類似,判斷常量是否是廢棄常量比較簡單,而要判定一個類是否是無用的類條件相對苛刻,類需要滿足下面三個條件才能算無用的類:
(1)該類所有實例都已被回收,也就是Java堆中不存在該類的任何實例。
(2)加載該類ClassLoader已經(jīng)被回收。
(3)該類對應(yīng)的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
虛擬機(jī)可以對滿足以上三個條件的無用類進(jìn)行回收,這里說的僅僅是”可以“,而并不是和對象一樣,不使用了就必然回收,是否對類進(jìn)行回收,部分虛擬機(jī)提供了參數(shù)可以進(jìn)行控制。所以對類的回收整體是比較難的。使方法區(qū)發(fā)生類導(dǎo)致的內(nèi)存溢出基本思路:在運(yùn)行時產(chǎn)生大量的類去填滿方法區(qū),也就是在運(yùn)行時動態(tài)產(chǎn)生很多的類,直到方法區(qū)內(nèi)存溢出。所以頻繁動態(tài)產(chǎn)生很多類時,需要注意方法區(qū)內(nèi)存溢出。

垃圾收集算法

因為垃圾算法的實現(xiàn)設(shè)計大量程序細(xì)節(jié),各個平臺的虛擬機(jī)操作內(nèi)存的方法各不相同,因此在本文中我們主要講講垃圾收集算法,并不涉及具體實現(xiàn)。

標(biāo)記清除算法
標(biāo)記清除算法.jpg

上圖為標(biāo)記-清除算法的示意圖。算法分為標(biāo)記和清除兩個階段,首先需要標(biāo)記可回收的內(nèi)存,然后再對可回收的內(nèi)存進(jìn)行回收。這樣做的2個不足分別是:
(1)效率問題:標(biāo)記和清除兩個過程的效率都不高;
(2)空間問題:標(biāo)記-清除之后產(chǎn)生大量不連續(xù)的空間碎片。
為了解決效率問題,復(fù)制算法出現(xiàn)了(適合新生代),為了解決內(nèi)存碎片問題,標(biāo)記-整理算法出現(xiàn)了(適合老年代)。

復(fù)制算法
復(fù)制算法.jpg

上圖是復(fù)制算法實現(xiàn)圖。它將內(nèi)存分為大小相等的2塊,每次只是使用其中一塊。當(dāng)一塊內(nèi)存用完之后,就將存活的對象復(fù)制到另外一塊內(nèi)存區(qū)域并將本塊內(nèi)存清理。這樣做的大大降低了內(nèi)存空間使用率。我們的HotSpot的年輕代就是使用復(fù)制算法,只不過它的比例不是1:1,而是8:1。

標(biāo)記-整理算法
標(biāo)記整理算法.jpg

如圖是標(biāo)記-整理算法示意圖。相比標(biāo)記-清除算法,不同的是整理過程。將存活對象移到一端,然后清除可回收對象。這樣做的明顯好處就是產(chǎn)生了連續(xù)的空間。

分代收集算法
分代收集算法.png

基于上面的幾種收集算法,當(dāng)前商業(yè)虛擬機(jī)基本采用的都是分代收集。結(jié)合了復(fù)制和標(biāo)記-整理的優(yōu)勢。一般做法是將Java堆分為新生代和老年代。由于新生代會不斷產(chǎn)生新生對象,因此采用了復(fù)制算法;而年老代的對象存活率較高,因此采用了標(biāo)記-整理算法。在新生代中,我們可以看到新生代=Eden+S0+S1;他們設(shè)計的默認(rèn)比例是8:1:1;這個參數(shù)是可以通過虛擬機(jī)參數(shù)進(jìn)行調(diào)整的。

垃圾收集器

如果說收集算法是內(nèi)存回收的方法論,那么垃圾收集器就是內(nèi)存回收的具體實現(xiàn),下圖是基于java虛擬機(jī)的HotSpot虛擬機(jī)垃圾收集器。一共是7種收集器,我們分別進(jìn)行介紹。


jvm_hotspot垃圾收集器.png
Serial/Serial Old

Serial最基本,發(fā)展歷史最悠久的收集器。具體原理讓我們看一張圖最明了


Serial-Serial Old收集器運(yùn)行示意圖.jpg

上圖是Serial和Serial Old 兩種垃圾收集器的運(yùn)行示意圖。其中Serial 和Serial Old的區(qū)別就是一個是運(yùn)行在年輕代一個是運(yùn)行在年老代。從圖中我們可以看到,他是一個單線程模式的垃圾收集器。這里不僅僅是說該收集器是使用單線程或者一個CPU去完成垃圾收集,更重要的是它在進(jìn)行垃圾收集的時候必須暫停用戶線程,直到收集完成,也就是Stop the World。

ParNew

ParNew收集器其實是Serial收集器的多線程版本。它也是運(yùn)行在年輕代中,如圖:


parnew.jpg

如圖,在新生代中采用的是多線程模式進(jìn)行垃圾收集同時也需要暫停用戶線程直到垃圾收集完畢。這種模式和Serial相比,CPU數(shù)量越多的情況下優(yōu)勢更加明顯。如果CPU數(shù)量很少,比如2個,那么這種收集效率可能比Serial更低,因為它存在線程交互的開銷。

Parallel Scavenge/Parallel Old

Parallel Scavenge也是一個新生代,多線程收集器。那么這種收集器和之前的ParNew有什么區(qū)別呢?區(qū)別還是有的,不然怎么會出來這種收集器呢?其實Parallel Scavenge收集器的特點是它關(guān)注一個可控的吞吐量。那么什么是吞吐量,我這邊也不做任何講述,直接上一個公式大家就知道了。
吞吐量=運(yùn)行用戶代碼時間/(運(yùn)行用戶代碼時間+垃圾收集時間);
是不是很明白?垃圾收集時間越短,吞吐量就是越大。這里就有一個問題:垃圾收集時間越短,一般來講收集的垃圾量就是越少,也就是回收的內(nèi)存量越小,那么總內(nèi)存一定的情況下,我們在一定時間內(nèi)回收的次數(shù)就是越多。這就需要我們控制好回收時間來制約回收次數(shù)了。
同時,這里也有一個Parallel Old收集器,顧名思義是Parallel Scavenge收集器的年老代版本。
如下圖:


Parallel.jpg
CMS

CMS(Concurrent Mark Sweep),從名字我們知道這是一款基于并發(fā)使用標(biāo)記清除算法的垃圾收集器。他是一款以獲得最短回收停頓時間為目標(biāo)的收集器。讓我們趕緊來看看他的實現(xiàn)吧。上圖


cms.jpg

如上圖,它分為以下步驟:
(1)初始標(biāo)記:僅僅標(biāo)記GC Roots能直接關(guān)聯(lián)到的對象,時間很短,阻塞用戶線程
(2)并發(fā)標(biāo)記:標(biāo)記可回收對象,和用戶線程并行。
(3)重新標(biāo)記:標(biāo)記在并發(fā)階段因用戶線程繼續(xù)運(yùn)行產(chǎn)生的可回收對象,修正并發(fā)標(biāo)記,此時是阻塞用戶線程。
(4)并發(fā)清理:使用標(biāo)記-清除算法將垃圾進(jìn)行清理。
這種收集算法存在3個缺點:
(1)對CPU資源敏感。一般并發(fā)執(zhí)行的程序?qū)PU數(shù)量都是比較敏感的
(2)無法處理浮動垃圾。在并發(fā)清理階段用戶線程還在執(zhí)行,這時產(chǎn)生的垃圾無法清理。
(3)由于標(biāo)記-清除算法產(chǎn)生大量的空間碎片。

G1

G1,Garbage-First是當(dāng)今收集器技術(shù)發(fā)展的最前沿成果之一,它是一款面向服務(wù)端的垃圾收集器。

G1.jpg

實現(xiàn)思路:將整個java堆劃分為多個大小相等的獨立區(qū)域(Region),G1跟蹤各個Region里面的垃圾堆積和價值大小,在后臺維護(hù)一個優(yōu)先列表,每次進(jìn)行優(yōu)先回收。那么Region不是孤立的。那么如何避免全堆掃描呢?
G1使用Remembered Set。每一個Region對應(yīng)一個Remembered Set,在虛擬機(jī)對Reference類型的數(shù)據(jù)進(jìn)行寫操作的時候,會檢查Reference引用的對象是否處于不同的Region中,如是則記錄到Remembered Set中。
G1收集器的運(yùn)作大致分為以下幾個步驟:
(1)初始標(biāo)記:標(biāo)記GC Roots能直接關(guān)聯(lián)到的對象,耗時短
(2)并發(fā)標(biāo)記:找出存活的對象,耗時長
(3)最終標(biāo)記:修正并發(fā)標(biāo)記
(4)篩選回收:根據(jù)用戶所期望的GC停頓時間來制定回收計劃
以上就是垃圾收集器體系以及運(yùn)行原理,Java自動內(nèi)存管理可以歸結(jié)為自動化解決了兩個問題,給對象分配內(nèi)存以及回收分配給對象的內(nèi)存,下面我們來一起探討內(nèi)存分配策略。

內(nèi)存分配策略

內(nèi)存分配策略主要有:
(1)對象優(yōu)先在新生代Eden分配,當(dāng)Eden區(qū)沒有足夠空間進(jìn)行分配時,虛擬機(jī)發(fā)起一次Minor GC(指發(fā)生在新生代的垃圾收集動作)。
(2)大對象直接進(jìn)入老年代。
(3)長期存活的對象直接進(jìn)入老年代,在新生代Eden區(qū)多次GC未被回收的對象認(rèn)為是長期存活的對象。
(4)動態(tài)對象年齡判定,多年年齡主要是指對象經(jīng)過多少次GC,根據(jù)設(shè)置的對象年齡讓對象進(jìn)入老年代,但是對象年齡可以動態(tài)設(shè)置,以讓對象進(jìn)入老年代。
(5)空間分配擔(dān)保:在進(jìn)行GC前,虛擬機(jī)先檢查老年代最大連續(xù)可用連續(xù)空間是否大于新生代所有對象總空間,以確保Minor GC是安全的??臻g分配擔(dān)保主要是保證進(jìn)行GC后,有足夠的空間可以存放已有對象。
以上就是JVM垃圾收集器與內(nèi)存分配策略相關(guān)內(nèi)容,部分內(nèi)容直接從小臘月虛擬機(jī)相關(guān)文章直接拷貝而來(節(jié)省打字時間)。

JVM學(xué)習(xí)資料

《深入理解Java虛擬機(jī)》
Java虛擬機(jī)原理圖解系列文章
小臘月虛擬機(jī)相關(guān)文章

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容