《深入理解Java虛擬機(jī)》第3章讀書筆記
本文介紹了如何判斷對(duì)象是否存活嘿歌,三種垃圾回收算法哪雕,分析比較了幾種垃圾收集器的特點(diǎn)。本文并非原創(chuàng)更振,是《深入理解Java虛擬機(jī)》第3章的整理炕桨、總結(jié)和補(bǔ)充。
對(duì)象已死肯腕?
垃圾收集器在對(duì)堆進(jìn)行回收前献宫,要先判斷哪些對(duì)象“存活”,哪些已經(jīng)“死去”实撒。
引用計(jì)數(shù)算法
給對(duì)象中添加一個(gè)引用計(jì)數(shù)器姊途,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器就加1知态;當(dāng)引用失效時(shí)捷兰,計(jì)數(shù)器就減1;任何時(shí)刻計(jì)數(shù)器為0的對(duì)象就是不可能再被使用的负敏。
主流的Java虛擬機(jī)里面沒有選用引用計(jì)數(shù)算法來管理內(nèi)存贡茅。
優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單,效率高其做。
缺點(diǎn):很難解決對(duì)象之間相互循環(huán)引用的問題友扰。
循環(huán)引用問題彤叉,如下代碼所示,
/**
* 源代碼出自《深入理解Java虛擬機(jī)》P62-63
* 循環(huán)引用
**/
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/**
* 這個(gè)成員屬性的唯一意義就是占點(diǎn)內(nèi)存村怪,以便能在GC日志中看清楚是否被回收過
*/
private byte[] bigSize = new byte[2 * _1MB];
/**
* 運(yùn)行參數(shù)
* -XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M
*/
public static void main(String[] args) {
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();
}
}
JVM參數(shù)設(shè)置了新生代為10MB甚负,運(yùn)行結(jié)果顯示柬焕,在第一次觸發(fā)GC時(shí),“5120K->576K(9216K)”回收了約4MB內(nèi)存梭域。意味著虛擬機(jī)并沒有因?yàn)閮蓚€(gè)對(duì)象相互引用就不回收它們斑举,這也側(cè)面說明了虛擬機(jī)并不是通過引用計(jì)數(shù)算法來判斷對(duì)象是否存活
可達(dá)性分析算法
通過一系列的稱為“GC Roots”的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索病涨,搜索走過的路徑稱為引用鏈(Reference Chain)富玷,當(dāng)一個(gè)對(duì)象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個(gè)對(duì)象不可達(dá))時(shí)既穆,則證明此對(duì)象是不可用的赎懦。
如圖,object5幻工、object6励两、object7 為可回收對(duì)象
主流的Java虛擬機(jī)使用可達(dá)性分析算法
在Java語言中,GC Roots包括以下幾種:
- 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象
- 方法區(qū)中類靜態(tài)屬性引用的對(duì)象
- 方法區(qū)中常量引用的對(duì)象
- 本地方法棧中Native方法引用的對(duì)象
四種引用
- 強(qiáng)引用:代碼中普遍存在囊颅,垃圾收集器不會(huì)回收強(qiáng)引用的對(duì)象当悔。
- 軟引用:有用但非必需,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前踢代,會(huì)把這些對(duì)象列入回收范圍盲憎。
- 弱引用:非必需,無論當(dāng)前內(nèi)存是否足夠胳挎,下次垃圾回收都會(huì)回收掉這些對(duì)象饼疙。
- 虛引用:最弱的引用關(guān)系,是否有虛引用不對(duì)其生存時(shí)間構(gòu)成影響串远。
垃圾收集算法
標(biāo)記-清除算法
最基礎(chǔ)的收集算法——“標(biāo)記-清除”(Mark-Sweep)算法宏多。一般用于老年代儿惫。
算法分為“標(biāo)記”和“清除”兩個(gè)階段:
- 標(biāo)記出需要回收的對(duì)象
- 清除被標(biāo)記的對(duì)象
缺點(diǎn):效率低澡罚,空間碎片化。
復(fù)制算法
為了解決效率問題肾请,出現(xiàn)了“復(fù)制”算法(Copying)留搔。它將內(nèi)存分為兩塊,每次只使用其中一塊铛铁,當(dāng)這一塊內(nèi)存用完了隔显,就將還存活的對(duì)象復(fù)制到另一塊上面却妨,然后再把已使用過的內(nèi)存空間一次清理掉。
缺點(diǎn):內(nèi)存縮小為原來的一半括眠。
JVM虛擬機(jī)在新生代使用這種收集算法彪标,并不是按照 1:1 的比例來劃分內(nèi)存空間。而是根據(jù)新生代中的對(duì)象98%是“朝生夕死”這一特點(diǎn)掷豺,將內(nèi)存分為一塊較大的 Eden(80%) 空間和兩塊較小的 Survivor(10%) 空間捞烟。每次只使用 Eden 和其中一塊 Survivor,當(dāng)回收時(shí)当船,將 Eden 和 Survivor 中還存活的對(duì)象题画,一次性地復(fù)制到另外一塊 Survivor 空間上,最后清理掉 Eden 和剛才用過的 Survivor 空間德频。
HotSpot默認(rèn)Eden和Survivor的大小比例為8:1苍息,這樣就只有10%的內(nèi)存被“浪費(fèi)”。當(dāng)出現(xiàn)超過10%的對(duì)象存活時(shí)壹置,就會(huì)使用老年代做分配擔(dān)保竞思,把Survivor空間放不下的對(duì)象,直接放入老年代蒸绩。
標(biāo)記-整理算法
在Mark-Sweep算法的基礎(chǔ)上做了改良衙四,用于解決空間碎片化問題。標(biāo)記-整理(Mark-Compact)算法在標(biāo)記后不是簡(jiǎn)單做清除患亿,而是讓所有存活的對(duì)象都向一端移動(dòng)传蹈,然后清理掉端邊界以外的內(nèi)存。一般用于老年代步藕。
安全點(diǎn)和安全區(qū)域
安全點(diǎn)
在做可達(dá)性分析時(shí)惦界,需要保持分析期間整個(gè)系統(tǒng)不會(huì)發(fā)生變化,這就導(dǎo)致GC進(jìn)行時(shí)必須停頓所有Java執(zhí)行線程(Stop The World)咙冗,即使是在號(hào)稱(幾乎)不會(huì)發(fā)生停頓的CMS收集器中沾歪,枚舉根節(jié)點(diǎn)時(shí)也必須要停頓。
程序執(zhí)行時(shí)并非在所有地方都能停下來開始GC,只有在到達(dá)安全點(diǎn)(Safepoint)時(shí)才能暫停洞渔。Safepoint 的選定既不能太少以致于讓GC等待時(shí)間太長(zhǎng)失都,也不能過于頻繁以致于過分增大運(yùn)行時(shí)的負(fù)荷。所以狂窑,安全點(diǎn)的選定基本上是以程序“是否具有讓程序長(zhǎng)時(shí)間執(zhí)行的特征”為標(biāo)準(zhǔn)進(jìn)行選定的,例如方法調(diào)用桑腮,循環(huán)跳轉(zhuǎn)泉哈,異常跳轉(zhuǎn)等。
如何在GC發(fā)生時(shí)讓線程都跑到最近的安全點(diǎn)再停頓下來?
- 搶先試中斷:先把所有線程中斷丛晦,發(fā)現(xiàn)不在安全點(diǎn)的線程恢復(fù)線程奕纫,讓它跑到安全點(diǎn)。
- 主動(dòng)式中斷:設(shè)置一個(gè)不可讀的內(nèi)存位置作為中斷標(biāo)志烫沙,標(biāo)志與安全點(diǎn)重合匹层,當(dāng)線程執(zhí)行到這個(gè)標(biāo)志時(shí)自己中斷掛起。
安全區(qū)域
安全區(qū)域(Safe Region)是指在一段代碼片段中锌蓄,引用關(guān)系不會(huì)發(fā)生變化又固。在這個(gè)區(qū)域的任何地方開始GC都是安全的。典型的安全區(qū)域比如線程處于Sleep狀態(tài)或者Blocked狀態(tài)煤率。
在線程執(zhí)行到Safe Region中的代碼時(shí)仰冠,首先標(biāo)識(shí)自己已經(jīng)進(jìn)入了Safe Region。當(dāng)要發(fā)起GC時(shí)蝶糯,就不用管標(biāo)識(shí)為Safe Region狀態(tài)的線程了洋只。當(dāng)線程要離開Safe Region時(shí),要檢查是否處于GC狀態(tài)昼捍,如果是识虚,就要繼續(xù)等待,直到收到可以安全離開Safe Region的信號(hào)為止妒茬。
垃圾收集器
并行與并發(fā)
- 并行(Parallel):指多條垃圾收集線程并行工作担锤,但此時(shí)用戶線程仍然處于等待狀態(tài)。
- 并發(fā)(Concurrent):指用戶線程與垃圾收集線程同時(shí)執(zhí)行(但不一定是并行的乍钻,可能會(huì)交替執(zhí)行)肛循,用戶程序在繼續(xù)運(yùn)行,而垃圾收集程序運(yùn)行于另一個(gè)CPU上银择。
吞吐量
吞吐量就是CPU用于運(yùn)行用戶代碼的時(shí)間與CPU總消耗時(shí)間的比值多糠,即
吞吐量 = 運(yùn)行用戶代碼時(shí)間 / (運(yùn)行用戶代碼時(shí)間 + 垃圾收集時(shí)間)。
假設(shè)虛擬機(jī)總共運(yùn)行了100分鐘浩考,其中垃圾收集花掉1分鐘夹孔,那吞吐量就是99%。
Minor GC 和 Full GC
- 新生代GC(Minor GC):指發(fā)生在新生代的垃圾收集動(dòng)作析孽,因?yàn)镴ava對(duì)象大多都具備朝生夕滅的特性搭伤,所以Minor GC 非常頻繁,一般回收速度也比較快袜瞬。
- 老年代GC(Major GC / Full GC):指發(fā)生在老年代的GC怜俐,出現(xiàn)了Major GC,經(jīng)常會(huì)伴隨至少一次的Minor GC(但非絕對(duì)的吞滞,在Parallel Scavenge收集器的收集策略里就有直接進(jìn)行Major GC的策略選擇過程)佑菩。Major GC的速度一般會(huì)比Minor GC慢10倍以上。
HotSpot虛擬機(jī)的垃圾收集器對(duì)比
收集器 | 串行裁赠、并行殿漠、并發(fā) | 新生代、老生代 | 算法 | 目標(biāo) | 使用場(chǎng)景 |
---|---|---|---|---|---|
Serial | 單線程佩捞,串行 | 新生代 | 復(fù)制算法 | 響應(yīng)速度優(yōu)先 | 單CPU環(huán)境下的Client模式 |
Serial Old | 單線程绞幌,串行 | 老年代 | 標(biāo)記-整理 | 響應(yīng)速度優(yōu)先 | 單CPU環(huán)境下的Client模式、CMS的后 |
ParNew | 多線程一忱,并行 | 新生代 | 復(fù)制算法 | 響應(yīng)速度優(yōu)先 | 多CPU環(huán)境時(shí)在Server模式下與CMS配合 |
Parallel Scanvenge | 多線程莲蜘,并行 | 新生代 | 復(fù)制算法 | 吞吐量?jī)?yōu)先 | 在后臺(tái)運(yùn)算而不需要太多交互的任務(wù) |
Parallel Old | 多線程,并行 | 老年代 | 標(biāo)記-整理 | 吞吐量?jī)?yōu)先 | 在后臺(tái)運(yùn)算而不需要太多交互的任務(wù) |
CMS | 并發(fā) | 老年代 | 標(biāo)記-清除 | 響應(yīng)速度優(yōu)先 | 集中在互聯(lián)網(wǎng)站或B/S系統(tǒng)服務(wù)端上的Java應(yīng)用 |
G1 | 并發(fā) | both | 復(fù)制算法+標(biāo)記-整理 | 響應(yīng)速度優(yōu)先 | 面向服務(wù)端應(yīng)用帘营,將來替換CMS |
ParNew 收集器
Serial收集器的多線程版本票渠,Service模式下的首選新生代收集器,除了Serial收集器外芬迄,目前只有它能與CMS收集器配合工作问顷。
ParNew 收集器是使用 -XX:+UseConcMarkSweepGC
選項(xiàng)后的默認(rèn)新生代收集器,也可以使用 -XX:+UseParNewGC
選項(xiàng)來強(qiáng)制指定它禀梳。
運(yùn)行示意圖如下:
CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器杜窄。基于Mark-Sweep算法算途。運(yùn)行過程分為四個(gè)部分:
- 初始標(biāo)記
- 并發(fā)標(biāo)記
- 重新標(biāo)記
- 并發(fā)清除
其中塞耕,初始標(biāo)記和重新標(biāo)記仍然需要 Stop The World。初始標(biāo)記只是標(biāo)記一下 GC Roots 能直接關(guān)聯(lián)到的對(duì)象嘴瓤,速度很快扫外。并發(fā)標(biāo)記就是進(jìn)行 GC Roots Tracing 的過程,而重新標(biāo)記階段則是為了修正并發(fā)標(biāo)記期間用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄廓脆,這個(gè)階段的停頓時(shí)間比初始標(biāo)記稍長(zhǎng)一些畏浆,但遠(yuǎn)比并發(fā)標(biāo)記的時(shí)間短。
由于整個(gè)過程中耗時(shí)最長(zhǎng)的并發(fā)標(biāo)記和并發(fā)清楚過程收集器線程都可以與用戶線程一起工作狞贱,所以刻获,從總體上來說,CMS收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)執(zhí)行的瞎嬉。
運(yùn)行示意圖如下:
CMS 收集器有如下3個(gè)缺點(diǎn):
-
對(duì)CPU資源非常敏感
因?yàn)椴l(fā)階段需要占用一個(gè)用戶線程蝎毡,如果CPU小于4個(gè),則會(huì)導(dǎo)致用戶程序的執(zhí)行速度下降大于25%氧枣,如果只有2個(gè)CPU沐兵,用戶程序執(zhí)行速度則會(huì)下降50%,這是讓人無法接收的便监。一般來說使用CMS收集器的服務(wù)器配置至少需要4個(gè)CPU扎谎。
-
無法處理浮動(dòng)垃圾
在并發(fā)清理過程中產(chǎn)生的垃圾稱為“浮動(dòng)垃圾”碳想。這些垃圾只能等待下次垃圾回收。因此毁靶,CMS 收集器不能像其他收集器那樣等到老年代幾乎被完全填滿了再進(jìn)行收集胧奔,需要預(yù)留一部分空間提供并發(fā)收集時(shí)的程序運(yùn)作使用。
-
內(nèi)存空間碎片化
CMS 收集器是基于Mark-Sweep算法预吆,這個(gè)算法會(huì)產(chǎn)生內(nèi)存空間碎片龙填。CMS 收集器提供了一個(gè)
-XX:+UseCMSCompactAtFullCollection
開關(guān)參數(shù)(默認(rèn)為開啟),用于在CMS收集器頂不住要進(jìn)行Full GC時(shí)開啟內(nèi)存碎片的合并整理過程拐叉。內(nèi)存整理的過程是無法并發(fā)執(zhí)行的岩遗,空間碎片問題沒有了,但停頓時(shí)間不得不變長(zhǎng)凤瘦。
使用多個(gè)收集器配置宿礁,JVM會(huì)怎么處理?
如過同時(shí)使用了四個(gè)組合配置蔬芥,這是時(shí)候就會(huì)報(bào)錯(cuò)
但是比如 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
這兩個(gè)配置同時(shí)存在就不會(huì)報(bào)錯(cuò)窘拯。
翻看源碼可知
有些配置項(xiàng)是可以并存的。
其實(shí)坝茎,在使用 UseConcMarkSweepGC
配置的時(shí)候涤姊,虛擬機(jī)默認(rèn)開啟了 UseParNewGC
所以在配置JVM時(shí),我們盡量顯式配置嗤放。比如要啟用 ParNew + CMS 組合可以配置為
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
理解GC日志
《深入理解Java虛擬機(jī)》閱讀筆記系列
深入理解Java虛擬機(jī)1——內(nèi)存區(qū)域
本文首發(fā)于我的個(gè)人博客 https://chaohang.top
作者 張小超
公眾號(hào)【超超不會(huì)飛】
轉(zhuǎn)載請(qǐng)注明出處
歡迎關(guān)注我的微信公眾號(hào) 【超超不會(huì)飛】思喊,獲取第一時(shí)間的更新。