講講 JVM 的內(nèi)存管理『非專業(yè)』
jvm 內(nèi)存布局
一類是每個(gè)線程所獨(dú)享的:
- PC Register:也稱為程序計(jì)數(shù)器, 記錄每個(gè)線程當(dāng)前執(zhí)行的指令信息(eg:當(dāng)前執(zhí)行到哪一條指令,下一條該取哪條指令)
- JVM Stack:也稱為虛擬機(jī)棧,記錄每個(gè)棧幀(Frame)中的局部變量、方法返回地址等。線程中每次有方法調(diào)用時(shí),會(huì)創(chuàng)建Frame,方法調(diào)用結(jié)束時(shí)Frame銷毀。
- Native Method Stack: 本地(原生)方法棧,顧名思義就是調(diào)用操作系統(tǒng)原生本地方法時(shí),所需要的內(nèi)存區(qū)域。
上述3類區(qū)域,生命周期與Thread相同,即:線程創(chuàng)建時(shí),相應(yīng)的內(nèi)存區(qū)創(chuàng)建,線程銷毀時(shí),釋放相應(yīng)內(nèi)存。
- Heap:即鼎鼎大名的堆內(nèi)存區(qū),也是GC垃圾回收的主站場(chǎng),用于存放類的實(shí)例對(duì)象及Arrays實(shí)例等。
注:Heap被所有線程共享,如果嚴(yán)格意義上摳字眼的話,也不完正確,事實(shí)上,由于TLAB的存在,為了防止并發(fā)對(duì)象分配時(shí),多個(gè)對(duì)象分配到同1塊內(nèi)存,heap中的TLAB區(qū)域,在分配時(shí),是被線程獨(dú)占寫入的。
- Method Area:方法區(qū),主要存放類結(jié)構(gòu)、類成員定義,static靜態(tài)成員等。
- Runtime Constant Pool:運(yùn)行時(shí)常量池,比如:字符串,int -128~127范圍的值等,它是Method Area中的一部分。
Heap、Method Area 都是在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建,虛擬機(jī)退出時(shí)釋放
哪些內(nèi)存區(qū)域需要 GC
thread獨(dú)享的區(qū)域:PC Regiester、JVM Stack、Native Method Stack,其生命周期都與線程相同(即:與線程共生死),所以無需GC。線程共享的Heap區(qū)、Method Area則是GC關(guān)注的重點(diǎn)對(duì)象。
引用類型
強(qiáng)引用:被強(qiáng)引用關(guān)聯(lián)的對(duì)象不會(huì)被回收。
軟引用:被軟引用關(guān)聯(lián)的對(duì)象只有在內(nèi)存不夠的情況下才會(huì)被回收。
弱引用:被弱引用關(guān)聯(lián)的對(duì)象一定會(huì)被回收,也就是說它只能存活到下一次垃圾回收發(fā)生之前。
虛引用:為一個(gè)對(duì)象設(shè)置虛引用的唯一目的是能在這個(gè)對(duì)象被回收時(shí)收到一個(gè)系統(tǒng)通知。
Minor GC 和 Full GC
Minor GC:回收新生代,因?yàn)樾律鷮?duì)象存活時(shí)間很短,因此 Minor GC 會(huì)頻繁執(zhí)行,執(zhí)行的速度一般也會(huì)比較快。
Minor GC,其觸發(fā)條件非常簡(jiǎn)單,當(dāng) Eden 空間滿時(shí),就將觸發(fā)一次 Minor GC。
Full GC:回收老年代和新生代,老年代對(duì)象其存活時(shí)間長(zhǎng),因此 Full GC 很少執(zhí)行,執(zhí)行速度會(huì)比 Minor GC 慢很多。
FULL GC 的觸發(fā)條件有以下幾個(gè):
「調(diào)用 System.gc()」
只是建議虛擬機(jī)執(zhí)行 Full GC,但是虛擬機(jī)不一定真正去執(zhí)行。不建議使用這種方式,而是讓虛擬機(jī)管理內(nèi)存。
「老年代空間不足」
老年代空間不足的常見場(chǎng)景為前文所講的大對(duì)象直接進(jìn)入老年代、長(zhǎng)期存活的對(duì)象進(jìn)入老年代等。
為了避免以上原因引起的 Full GC,1.應(yīng)當(dāng)盡量不要?jiǎng)?chuàng)建過大的對(duì)象以及數(shù)組。2.除此之外,可以通過 -Xmn 虛擬機(jī)參數(shù)調(diào)大新生代的大小,讓對(duì)象盡量在新生代被回收掉,不進(jìn)入老年代。3.還可以通過 -XX:MaxTenuringThreshold 調(diào)大對(duì)象進(jìn)入老年代的年齡,讓對(duì)象在新生代多存活一段時(shí)間。
「空間分配擔(dān)保失敗」
使用復(fù)制算法的 Minor GC 需要老年代的內(nèi)存空間作擔(dān)保,如果擔(dān)保失敗會(huì)執(zhí)行一次 Full GC。
「JDK 1.7 及以前的永久代空間不足」
在 JDK 1.7 及以前,HotSpot 虛擬機(jī)中的方法區(qū)是用永久代實(shí)現(xiàn)的,永久代中存放的為一些 Class 的信息、常量、靜態(tài)變量等數(shù)據(jù)。
當(dāng)系統(tǒng)中要加載的類、反射的類和調(diào)用的方法較多時(shí),永久代可能會(huì)被占滿,在未配置為采用 CMS GC 的情況下也會(huì)執(zhí)行 Full GC。如果經(jīng)過 Full GC 仍然回收不了,那么虛擬機(jī)會(huì)拋出 java.lang.OutOfMemoryError。
為避免以上原因引起的 Full GC,可采用的方法為增大永久代空間或轉(zhuǎn)為使用 CMS GC。
「Concurrent Mode Failure」
執(zhí)行 CMS GC 的過程中同時(shí)有對(duì)象要放入老年代,而此時(shí)老年代空間不足(可能是 GC 過程中浮動(dòng)垃圾過多導(dǎo)致暫時(shí)性的空間不足),便會(huì)報(bào) Concurrent Mode Failure 錯(cuò)誤,并觸發(fā) Full GC。
如何判斷對(duì)象是垃圾
引用計(jì)數(shù)算法
在兩個(gè)對(duì)象出現(xiàn)循環(huán)引用的情況下,此時(shí)引用計(jì)數(shù)器永遠(yuǎn)不為 0,導(dǎo)致無法對(duì)它們進(jìn)行回收。正是因?yàn)檠h(huán)引用的存在,因此 Java 虛擬機(jī)不使用引用計(jì)數(shù)算法。
- 可達(dá)性分析算法
以 GC Roots 為起始點(diǎn)進(jìn)行搜索,可達(dá)的對(duì)象都是存活的,不可達(dá)的對(duì)象可被回收。
Java 虛擬機(jī)使用該算法來判斷對(duì)象是否可被回收,GC Roots 一般包含以下內(nèi)容:
- 虛擬機(jī)棧中局部變量表中引用的對(duì)象
- 本地方法棧中 JNI 中引用的對(duì)象
- 方法區(qū)中類靜態(tài)屬性引用的對(duì)象
- 方法區(qū)中的常量引用的對(duì)象
除了對(duì)象回收之外,還可能會(huì)有類的卸載
方法區(qū)主要存放永久代對(duì)象,而永久代對(duì)象的回收率比新生代低很多,所以在方法區(qū)上進(jìn)行回收性價(jià)比不高。方法區(qū)的回收主要是對(duì)常量池的回收和對(duì)類的卸載。
為了避免內(nèi)存溢出,在大量使用反射和動(dòng)態(tài)代理的場(chǎng)景都需要虛擬機(jī)具備類卸載功能。類的卸載條件很多,需要滿足以下三個(gè)條件,并且滿足了條件也不一定會(huì)被卸載:
- 該類所有的實(shí)例都已經(jīng)被回收,此時(shí)堆中不存在該類的任何實(shí)例。
- 加載該類的 ClassLoader 已經(jīng)被回收。
- 該類對(duì)應(yīng)的 Class 對(duì)象沒有在任何地方被引用,也就無法在任何地方通過反射訪問該類方法。
finalize()
- 類似 C++ 的析構(gòu)函數(shù),用于關(guān)閉外部資源。但是 try-finally 等方式可以做得更好,并且該方法運(yùn)行代價(jià)很高,不確定性大,無法保證各個(gè)對(duì)象的調(diào)用順序,因此最好不要使用。
- 當(dāng)一個(gè)對(duì)象可被回收時(shí),如果需要執(zhí)行該對(duì)象的 finalize() 方法,那么就有可能在該方法中讓對(duì)象重新被引用,從而實(shí)現(xiàn)自救。自救只能進(jìn)行一次,如果回收的對(duì)象之前調(diào)用了 finalize() 方法自救,后面回收時(shí)不會(huì)再調(diào)用該方法。
常用的 GC 算法
「標(biāo)記清除法」:在標(biāo)記階段,程序會(huì)檢查每個(gè)對(duì)象是否為活動(dòng)對(duì)象,如果是活動(dòng)對(duì)象,則程序會(huì)在對(duì)象頭部打上標(biāo)記。
在清除階段,會(huì)進(jìn)行對(duì)象回收并取消標(biāo)志位,另外,還會(huì)判斷回收后的分塊與前一個(gè)空閑分塊是否連續(xù),若連續(xù),會(huì)合并這兩個(gè)分塊。回收對(duì)象就是把對(duì)象作為分塊,連接到被稱為 “空閑鏈表” 的單向鏈表,之后進(jìn)行分配時(shí)只需要遍歷這個(gè)空閑鏈表,就可以找到分塊。
優(yōu)缺點(diǎn):
- 標(biāo)記和清除過程效率都不高;
- 會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,導(dǎo)致無法給大對(duì)象分配內(nèi)存。
「標(biāo)記復(fù)制法」:思路也很簡(jiǎn)單,將內(nèi)存對(duì)半分,總是保留一塊空著(上圖中的右側(cè)),將左側(cè)存活的對(duì)象(淺灰色區(qū)域)復(fù)制到右側(cè),然后左側(cè)全部清空。
優(yōu)缺點(diǎn):
- 避免了內(nèi)存碎片問題。
- 內(nèi)存浪費(fèi)很嚴(yán)重,相當(dāng)于只能使用50%的內(nèi)存。
現(xiàn)在的商業(yè)虛擬機(jī)都采用這種收集算法回收新生代,但是并不是劃分為大小相等的兩塊,而是一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor。在回收時(shí),將 Eden 和 Survivor 中還存活著的對(duì)象全部復(fù)制到另一塊 Survivor 上,最后清理 Eden 和使用過的那一塊 Survivor。
HotSpot 虛擬機(jī)的 Eden 和 Survivor 大小比例默認(rèn)為 8:1,保證了內(nèi)存的利用率達(dá)到 90%。如果每次回收有多于 10% 的對(duì)象存活,那么一塊 Survivor 就不夠用了,此時(shí)需要依賴于老年代進(jìn)行空間分配擔(dān)保,也就是借用老年代的空間存儲(chǔ)放不下的對(duì)象。
「標(biāo)記-整理(也稱標(biāo)記-壓縮)法」:避免了上述二種算法的缺點(diǎn),將垃圾對(duì)象清理掉后,同時(shí)將剩下的存活對(duì)象進(jìn)行整理挪動(dòng)(類似于windows的磁盤碎片整理),保證它們占用的空間連續(xù),這樣就避免了內(nèi)存碎片問題,但是整理過程也會(huì)降低GC的效率.
「generation-collect 分代收集算法」:經(jīng)過大量實(shí)際分析,發(fā)現(xiàn)內(nèi)存中的對(duì)象,大致可以分為二類:有些生命周期很短,比如一些局部變量/臨時(shí)對(duì)象,而另一些則會(huì)存活很久(典型的,比如websocket長(zhǎng)連接中的connection對(duì)象)?;舅枷胧菍?nèi)存分成了三大塊:年青代(Young Genaration),老年代(Old Generation),永久代(Permanent Generation),其中Young Genaration更是又細(xì)為分eden,S0, S1三個(gè)區(qū)。
剛開始時(shí),對(duì)象分配在eden區(qū),s0及s1區(qū),幾乎是空著的。當(dāng)eden區(qū)放不下時(shí),就會(huì)發(fā)生minor GC(也被稱為young GC),第1步當(dāng)然是要先標(biāo)識(shí)出不可達(dá)垃圾對(duì)象,然后講可達(dá)對(duì)象移到 s0 區(qū)。之后當(dāng) eden 區(qū)又滿了之后,s0 和 eden 區(qū)的可達(dá)對(duì)象將會(huì)都移到 s1 區(qū)。之后 s0 和 s1 區(qū)的對(duì)象會(huì)相互移來移去,每移動(dòng) 1 次,他們的年齡會(huì) +1。所以當(dāng)它們的年齡到達(dá)一定區(qū)域之后,將會(huì)移到老年代。如果老年代也滿了,那么將會(huì)移到永久代。
- 新生代使用:復(fù)制算法
- 老年代使用:標(biāo)記 - 清除 或者 標(biāo)記 - 整理 算法
垃圾收集器
https://www.jianshu.com/p/b572f69a1b93
**新生代垃圾收集器有Serial、ParNew、Parallel Scavenge,G1,屬于老年代的垃圾收集器有CMS、Serial Old、Parallel Old和G1。**其中的G1是一種既可以對(duì)新生代對(duì)象也可以對(duì)老年代對(duì)象進(jìn)行回收的垃圾收集器。然而,在所有的垃圾收集器中,并沒有一種普遍使用的垃圾收集器。在不同的場(chǎng)景下,每種垃圾收集器有各自的優(yōu)勢(shì),如下圖:
- 「Serial收集器」
**單線程垃圾收集器,**這就意味著在其進(jìn)行垃圾收集的時(shí)候需要暫停其他的線程。
收集過程:暫停所有線程 算法:復(fù)制算法 優(yōu)點(diǎn):簡(jiǎn)單高效,擁有很高的單線程收集效率 應(yīng)用:Client模式下的默認(rèn)新生代收集器
- 「ParNew收集器」
理解為**Serial收集器的多線程版本,由于存在線程切換的開銷,**ParNew在單CPU的環(huán)境中比不上Serial(ParNew收集線程數(shù)與CPU的數(shù)量相同, 因此在CPU數(shù)量過大的環(huán)境中, 可用-XX:ParallelGCThreads參數(shù)控制GC線程數(shù))。
收集過程:暫停所有線程 算法:復(fù)制算法 優(yōu)點(diǎn):在CPU多的情況下,擁有比Serial更好的效果。單CPU環(huán)境下Serial效果更好 應(yīng)用:許多運(yùn)行在Server模式下的虛擬機(jī)中首選的新生代收集器
- 「Parallel Scavenge 收集器」
類似ParNew收集器,**Parallel收集器更關(guān)注系統(tǒng)的吞吐量。**區(qū)別在于Parallel Scavenge收集器更關(guān)注可控制的吞吐量(「吞吐量 = 運(yùn)行用戶代碼的時(shí)間/(運(yùn)行用戶代碼的時(shí)間+垃圾收集時(shí)間)」)。吞吐量越大,意味著垃圾收集的時(shí)間越短,則用戶代碼則可以充分利用CPU資源,盡快完成程序的運(yùn)算任務(wù)。
-XX:MaxGCPauseMillis 控制最大的垃圾收集停頓時(shí)間,-XX:GCRatio 直接設(shè)置吞吐量的大小。
-XX:+UseAdaptiveSizePocily 來動(dòng)態(tài)調(diào)整停頓時(shí)間或者最大的吞吐量,這種方式稱為GC自適應(yīng)調(diào)節(jié)策略,這點(diǎn)是ParNew收集器所沒有的。
- 「Serial Old收集器」
「Serial Old收集器是Serial收集器的老年代版本」,也是一個(gè)單線程收集器,采用“「標(biāo)記-整理算法」”進(jìn)行回收。其運(yùn)行過程與Serial收集器一樣。
- 「Parallel Old收集器」
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和“「標(biāo)記-整理」”算法進(jìn)行垃圾回收。
通常與Parallel Scavenge收集器配合使用,“吞吐量?jī)?yōu)先”收集器是這個(gè)組合的特點(diǎn),在注重吞吐量和CPU資源敏感的場(chǎng)合,都可以使用這個(gè)組合。
- 「CMS 收集器」
CMS(Concurrent Mark Sweep)收集器是一種「以獲取最短回收停頓時(shí)間為目標(biāo)的收集器」。目前很大一部分的Java應(yīng)用都集中在互聯(lián)網(wǎng)站或B/S系統(tǒng)的服務(wù)端上,這類應(yīng)用尤其重視服務(wù)的響應(yīng)速度,希望系統(tǒng)停頓時(shí)間最短,以給用戶帶來較好的體驗(yàn)。
「基于“標(biāo)記-清除”算法實(shí)現(xiàn)的」,它的運(yùn)作過程相對(duì)于前面幾種收集器來說要更復(fù)雜一些,整個(gè)過程分為4個(gè)步驟,包括:
其中**初始標(biāo)記、重新標(biāo)記這兩個(gè)步驟仍然需要“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í)間一般會(huì)比初始標(biāo)記階段稍長(zhǎng)一些,但遠(yuǎn)比并發(fā)標(biāo)記的時(shí)間短。
「由于整個(gè)過程中耗時(shí)最長(zhǎng)的并發(fā)標(biāo)記和并發(fā)清除過程中,收集器線程都可以與用戶線程一起工作,所以總體上來說,CMS收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)地執(zhí)行?!?/p>
優(yōu)缺點(diǎn):
- 并發(fā)收集、低停頓
- 產(chǎn)生大量空間碎片、并發(fā)階段會(huì)降低吞吐量
- 初始標(biāo)記(CMS initial mark)
- 并發(fā)標(biāo)記(CMS concurrent mark)
- 重新標(biāo)記(CMS remark)
- 并發(fā)清除(CMS concurrent sweep)
「G1 收集器」(整個(gè)Java堆:包括新生代和老年代)
G1 的特點(diǎn)是:采用并發(fā)與并行、空間整合(整體上類似標(biāo)記-整理方法,不會(huì)產(chǎn)生內(nèi)存碎片)、分代收集、「可預(yù)測(cè)的停頓」(比CMS更先進(jìn)的地方在于能讓使用者明確指定一個(gè)長(zhǎng)度為M毫秒的時(shí)間片段內(nèi),消耗在垃圾收集上的時(shí)間不得超過N毫秒)。
G1收集器將Java堆劃分為多個(gè)大小相等的Region(獨(dú)立區(qū)域),新生代與老年代都是一部分Region的集合,G1的收集范圍則是這一個(gè)個(gè)Region。
整個(gè)工作流程:初始標(biāo)記、并發(fā)標(biāo)記、最終標(biāo)記、篩選回收。初始標(biāo)記階段僅僅只是標(biāo)記一下GC Roots能夠直接關(guān)聯(lián)的對(duì)象,并且修改TAMS(Next Top at Mark Start)的值,讓下一階段的用戶程序并發(fā)運(yùn)行的時(shí)候,能在正確可用的Region中創(chuàng)建對(duì)象,這個(gè)階段需要暫停線程。并發(fā)標(biāo)記階段從GC Roots進(jìn)行可達(dá)性分析,找出存活的對(duì)象,這個(gè)階段是與用戶線程并發(fā)執(zhí)行的。最終標(biāo)記階段則是修正在并發(fā)標(biāo)記階段因?yàn)橛脩舫绦虻牟l(fā)執(zhí)行而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分記錄,這部分記錄被保存在Remembered Set Logs中,最終標(biāo)記階段再把Logs中的記錄合并到Remembered Set中,這個(gè)階段是并行執(zhí)行的,仍然需要暫停用戶線程。最后在篩選階段首先對(duì)各個(gè)Region的回收價(jià)值和成本進(jìn)行排序,根據(jù)用戶所期望的GC停頓時(shí)間制定回收計(jì)劃。
內(nèi)存分配策略
- 對(duì)象優(yōu)先在 Eden 分配
大多數(shù)情況下,對(duì)象在新生代 Eden 上分配,當(dāng) Eden 空間不夠時(shí),發(fā)起 Minor GC。
- 大對(duì)象直接進(jìn)入老年代
大對(duì)象是指需要連續(xù)內(nèi)存空間的對(duì)象,最典型的大對(duì)象是那種很長(zhǎng)的字符串以及數(shù)組。
經(jīng)常出現(xiàn)大對(duì)象會(huì)提前觸發(fā)垃圾收集以獲取足夠的連續(xù)空間分配給大對(duì)象。
-XX:PretenureSizeThreshold,大于此值的對(duì)象直接在老年代分配,避免在 Eden 和 Survivor 之間的大量?jī)?nèi)存復(fù)制。
- 長(zhǎng)期存活的對(duì)象進(jìn)入老年代
為對(duì)象定義年齡計(jì)數(shù)器,對(duì)象在 Eden 出生并經(jīng)過 Minor GC 依然存活,將移動(dòng)到 Survivor 中,年齡就增加 1 歲,增加到一定年齡則移動(dòng)到老年代中。
-XX:MaxTenuringThreshold 用來定義年齡的閾值。
- 動(dòng)態(tài)對(duì)象年齡判斷
虛擬機(jī)并不是永遠(yuǎn)要求對(duì)象的年齡必須達(dá)到 MaxTenuringThreshold 才能晉升老年代,如果在 Survivor 中相同年齡所有對(duì)象大小的總和大于 Survivor 空間的一半,則年齡大于或等于該年齡的對(duì)象可以直接進(jìn)入老年代,無需等到 MaxTenuringThreshold 中要求的年齡。
- 空間分配擔(dān)保
在發(fā)生 Minor GC 之前,虛擬機(jī)先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對(duì)象總空間,如果條件成立的話,那么 Minor GC 可以確認(rèn)是安全的。
如果不成立的話虛擬機(jī)會(huì)查看 HandlePromotionFailure 的值是否允許擔(dān)保失敗,如果允許那么就會(huì)繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對(duì)象的平均大小,如果大于,將嘗試著進(jìn)行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允許冒險(xiǎn),那么就要進(jìn)行一次 Full GC。
巨人的肩膀
https://github.com/CyC2018/CS-Notes
本文轉(zhuǎn)載自微信公眾號(hào)「多選參數(shù)」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系多選參數(shù)公眾號(hào)。





























