JVM 垃圾收集器與內(nèi)存分配策略
由JVM內(nèi)存區(qū)域可知Java運(yùn)行時內(nèi)存的各個區(qū)域喘批。其中程序計數(shù)器研儒、虛擬機(jī)棧换途、本地方法棧3個區(qū)域隨線程而生棒仍,隨線程而滅缨历,當(dāng)方法結(jié)束或者線程結(jié)束時缔御,內(nèi)存就會跟著被回收了朝氓。
而只有處于運(yùn)行期間径荔,我們才能知道程序究竟會創(chuàng)建哪些對象垂寥,創(chuàng)建多少個對象颠黎,所以Java堆和方法區(qū)這兩個區(qū)域內(nèi)存的分配和回收時動態(tài)的,垃圾收集器也只關(guān)注這部分內(nèi)存的管理滞项。
一.對象存活判斷
1.引用計數(shù)算法
在對象中添加一個引用計數(shù)器狭归,每當(dāng)有一個地方引用它時,計數(shù)器值就加一文判;當(dāng)引用失效時过椎,計數(shù)器值就減一;任何時候計數(shù)器值為零的對象就是不可能再被使用的戏仓。
- 優(yōu)點:實現(xiàn)簡單疚宇,效率高
- 缺點:無法解決對象相互循環(huán)引用的問題——會導(dǎo)致對象的引用雖然存在,但是已經(jīng)不可能再被使用赏殃,卻無法被回收敷待。
2.可達(dá)性分析算法
通過一系列的稱為”GC Roots”的對象作為起始點, 從這些節(jié)點開始向下搜索, 搜索走過的路徑稱為引用鏈(Reference Chain), 當(dāng)一個對象到GC Roots不可達(dá)(也就是不存在引用鏈)的時候, 證明對象是不可用的。如下圖: Object5仁热、6榜揖、7 雖然互有關(guān)聯(lián), 但它們到GC Roots是不可達(dá)的, 因此也會被判定為可回收的對象。
在Java, 可作為GC Roots的對象包括:
- 在虛擬機(jī)棧中引用的對象
- 在方法區(qū)中類靜態(tài)屬性引用的對象
- 在方法區(qū)中常量引用的對象
- 在本地方法棧中JNI(Native方法)引用的對象
- Java虛擬機(jī)內(nèi)部的引用
- 所有被同步鎖(synchronized關(guān)鍵字)持有的對象
二.如何回收(垃圾收集算法)
分代收集理論:
- 弱分代假說:絕大多數(shù)對象都是朝生夕滅的抗蠢。
- 強(qiáng)分代假說:熬過多次垃圾收集的對象就越難以消亡根盒。
Java堆劃分為新生代和老生代兩個區(qū)域。在新生代中物蝙,每次垃圾收集時都發(fā)現(xiàn)有大批對象死去炎滞,而每次回收后存活的少量對象,將會逐步晉升到老年代中存放诬乞。
1.標(biāo)記-清除算法
分為標(biāo)記和清除兩個階段先標(biāo)記出需要回收的對象(可達(dá)性分析算法或者引用計數(shù)算法)册赛,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對象钠导。
缺點:
- 效率問題,標(biāo)記和清除效率都不高森瘪。
-
空間問題牡属,標(biāo)記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會導(dǎo)致以后在程序運(yùn)行過程中分配較大對象時無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾回收動作扼睬。
image.png
2.標(biāo)記-復(fù)制算法
將可用內(nèi)存劃分為大小相等的兩塊逮栅,每次只使用其中的一塊。當(dāng)這塊用完了窗宇,就將還存活的復(fù)制到另一塊上措伐,然后將已使用過的另一半內(nèi)存一次性清除。
新生代中的對象98%都是朝生夕死的军俊。將內(nèi)存分為較大Eden和兩個較小的survivor空間侥加。每次使用Eden和其中一個survivor,回收時將存活的對象一次性地復(fù)制到另一塊survivor中粪躬,再清理掉Eden和已用過的survivor担败。
HotSpot虛擬機(jī)Eden與Survivor默認(rèn)的大小比例為8:1:1。即只讓10%的新生代被浪費的镰官,survivor空間不夠時提前,需要依賴其他內(nèi)存(老年代)進(jìn)行分配擔(dān)保,即讓對象進(jìn)入老年代泳唠。
優(yōu)點:
- 對整個半?yún)^(qū)進(jìn)行回收狈网,不會出現(xiàn)空間碎片。
- 如果內(nèi)存中多數(shù)對象都是可回收警检,就只需復(fù)制少數(shù)的存活對象。
缺點:
- 如果內(nèi)存中多數(shù)對象都是存活的害淤,將產(chǎn)生大量的內(nèi)存間復(fù)制的開銷扇雕。
- 可用內(nèi)存縮減了一半,造成空間浪費窥摄。
3.標(biāo)記-整理算法
復(fù)制在對象存活率較高時效率很低镶奉。根據(jù)老年代的特點提出該算法。標(biāo)記過程同標(biāo)記清除一樣崭放,但不是直接對可回收對象進(jìn)行清理哨苛,而是讓存活對象朝著一端移動,然后直接清理掉邊界以外的內(nèi)存币砂。
標(biāo)記-整理與標(biāo)記-清除的差異在于整理是移動式的回收算法建峭,清除是非移動的。
根據(jù)各年代特點分別采用最適當(dāng)?shù)腉C算法决摧。
在新生代中每次垃圾收集都能發(fā)現(xiàn)大批對象已死, 只有少量存活亿蒸,因此選用標(biāo)記-復(fù)制算法, 只需要復(fù)制少量存活對象就可以完成收集凑兰。
在老年代因為對象存活率高、沒有額外空間對它進(jìn)行分配擔(dān)保, 就必須采用“標(biāo)記—清理”或“標(biāo)記—整理”算法來進(jìn)行回收, 不必進(jìn)行內(nèi)存復(fù)制, 且直接騰出空閑內(nèi)存边锁。即:
- 新生代:存活率低姑食,使用復(fù)制算法
- 老年代:存活率高,使用“標(biāo)記-整理”或“標(biāo)記-清除”算法
三.垃圾收集器
- 新生代:Serial收集器 ParNew收集器 Parallel Scavenge收集器
- 老年代:Serial Old收集器 Parallel Old收集器 CMS收集器
新生代:
1.Serial收集器
標(biāo)記復(fù)制茅坛。單線程收集器音半,只會使用一個處理器或一條收集線程進(jìn)行垃圾收集,而且在垃圾收集時贡蓖,必須暫停其他所有工作線程曹鸠,直到收集結(jié)束。優(yōu)點:簡單高效摩梧。
2.ParNew收集器
標(biāo)記復(fù)制物延。是Serial收集器的多線程并行版本,同時使用多條線程進(jìn)行垃圾收集仅父,其余與Serial一樣叛薯。目前唯一能與CMS收集器配合工作。
3.Parallel Scavenge收集器
標(biāo)記復(fù)制笙纤。并行收集的多線程收集器耗溜。CMS等收集器的關(guān)注點是盡可能地縮短垃圾收集時用戶線程的停頓時間,而PS收集器的目的則是達(dá)到一個可控制的吞吐量省容。吞吐量即CPU用于運(yùn)行用戶代碼的時間與CPU總消耗時間的比值(吞吐量=運(yùn)行用戶代碼的時間/(運(yùn)行用戶代碼的時間+垃圾收集的時間))抖拴。
老年代:
4.Serial Old收集器
標(biāo)記整理。Serial 收集器的老年代版本腥椒,單線程收集器阿宅。
5.Parallel Old收集器
標(biāo)記整理。Parallel Old是Parallel Scavenge的老年代版本笼蛛,多線程收集器洒放。
6.CMS收集器(Concurrent Mark Sweep并發(fā)標(biāo)記清除)
標(biāo)記清除。
并發(fā)收集滨砍,回收停頓時間短往湿。
步驟:
- 初始標(biāo)記:停掉用戶其他線程,僅標(biāo)記GCRoots能直接關(guān)聯(lián)到的對象惋戏,速度很快领追。
- 并發(fā)標(biāo)記:從GCRoots的直接關(guān)聯(lián)對象開始遍歷整個對象圖的過程,耗時長但不需要停頓用戶線程响逢,與垃圾收集線程一起并發(fā)執(zhí)行绒窑。
- 重新標(biāo)記:修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分對象的標(biāo)記記錄。耗時比初始標(biāo)記長一點舔亭,但遠(yuǎn)低于并發(fā)標(biāo)記回论。
- 并發(fā)清除:清理刪除掉那些標(biāo)記階段判斷為死亡的對象散罕,因為標(biāo)記清楚算法不需要移動存活對象,所以這個階段也是可以與用戶線程同時并發(fā)的傀蓉。
總體而言欧漱,CMS收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)執(zhí)行的。
缺點:
- CMS對處理器資源非常敏感葬燎。
- CMS無法處理浮動垃圾(Floating Garbage)误甚,可能出現(xiàn)Concurrent Mode Failure失敗而導(dǎo)致另一次Full GC的產(chǎn)生。
- CMS是標(biāo)記清除谱净,會產(chǎn)生大量碎片空間窑邦,對大對象內(nèi)存分配帶來麻煩。
7.G1收集器(Garbage First)
與其他收集器不同壕探,G1把連續(xù)的Java堆劃分為多個大小相等的獨立區(qū)域(Region)冈钦,每一個Region都可以根據(jù)需要扮演新勝達(dá)的Eden空間、Survivor空間李请,或者老年代空間瞧筛。收集器對不同角色的Region采用不同的策略去處理。優(yōu)先處理回收價值收益最大的那些Region导盅,也就是Garbage First的由來较幌。
- 從整體來看:“標(biāo)記-整理” 算法
- 從局部(兩個Region之間)來看:“復(fù)制”算法
四.內(nèi)存分配與回收策略
堆內(nèi)存劃分為 Eden、Survivor 和 Tenured/Old 空間白翻,如下圖所示:
image.png從年輕代空間(包括 Eden 和 Survivor 區(qū)域)回收內(nèi)存被稱為 Minor GC乍炉,對老年代GC稱為Major GC,而Full GC是對整個堆來說的。
- 新生代GC(Minor GC):發(fā)生在新生代的垃圾收集動作滤馍,非常頻繁岛琼,一般回收速度也比較快。
- 老年代GC(Major GC/Full GC):發(fā)生在老年代的垃圾收集動作巢株,一般會伴隨Minor GC 速度一般比Minor GC慢上10倍以上槐瑞。
對象的內(nèi)存分配從大方向講,就是在堆上分配纯续,對象主要分配在新生代的Eden區(qū)上随珠,如果啟動了本地線程分配緩沖灭袁,將按線程優(yōu)先在TLAB上分配猬错,少數(shù)情況下也有可能直接分配在老年代中,分配的規(guī)則并非百分之百固定的茸歧,細(xì)節(jié)取決于當(dāng)前使用的是哪一種垃圾收集器的組合倦炒,還有虛擬機(jī)中與內(nèi)存相關(guān)的參數(shù)設(shè)置。
1.對象優(yōu)先分配在Eden
大多數(shù)情況下软瞎,對象在新生代的Eden區(qū)分配逢唤,當(dāng)Eden區(qū)沒有足夠空間時拉讯,虛擬機(jī)將發(fā)起一次Minor GC。
2.大對象直接進(jìn)入老年代
所謂大對象是指需要大量連續(xù)內(nèi)存空間的對象鳖藕,最典型的大對象就是那種很長的字符串以及數(shù)組魔慷。大對象對虛擬機(jī)的內(nèi)存分配來說就是一個壞消息,經(jīng)常出現(xiàn)大對象容易導(dǎo)致內(nèi)存還有不少空間時就提前觸發(fā)GC以獲取足夠的連續(xù)空間來“安置”它們著恩。
3.長期存活的對象將進(jìn)入老年代
虛擬機(jī)給每個對象定義了一個對象年齡(Age)計數(shù)器院尔。如果對象在Eden出生并經(jīng)過第一次Minor GC后仍然存活,并且能夠被Survivor容納喉誊,將被移動到Survivor空間中邀摆,并且對象年齡為1.對象在Survivor區(qū)每“熬過”一次Minor GC,年齡就增加一歲伍茄,當(dāng)年齡增加到一定程度(默認(rèn)為15歲)栋盹,就將被晉升到老年代中。對象晉升老年代年齡的閾值敷矫,可以通過參數(shù)設(shè)置例获。
4.動態(tài)對象年齡判定
如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或者等于該年齡的對象就可以直接進(jìn)入老年代沪饺,無需等到要求的年齡
5.空間分配擔(dān)保
發(fā)生Minor GC之前躏敢,虛擬機(jī)會檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象的總空間,如果成立整葡,那么Minor GC確保是安全的件余,如果不成立,則虛擬機(jī)會查看HandlePromotionFailure設(shè)置值是否允許擔(dān)保失敗遭居。如果允許啼器,那么會繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對象的平均大小,如果大于俱萍,將嘗試進(jìn)行一次Minor GC端壳,盡管是有風(fēng)險的,如果小于或者設(shè)置不允許冒險枪蘑,那這時也要進(jìn)行一次Full GC损谦。