九神帶你入門JVM(下)

我們接著上面一篇繼續(xù)學習JVM的基本知識。

對象存活判斷

上篇中我們介紹過JVM垃圾回收綜述中說過一次垃圾回收之后會有一些對象存活。這節(jié)我們介紹兩個判斷對象存活的算法。

判斷對象存活有引用計數(shù)算法和可達性分析算法。

1、引用計數(shù)算法

給每一個對象添加一個引用計數(shù)器,每當有一個地方引用它時,計數(shù)器值加1;每當有一個地方不再引用它時,計數(shù)器值減1,這樣只要計數(shù)器的值不為0,就說明還有地方引用它,它就不是無用的對象。

這種方法看起來非常簡單,但目前許多主流的虛擬機都沒有選用這種算法來管理內(nèi)存,原因就是當某些對象之間互相引用時,無法判斷出這些對象是否已死。如下圖,對象1和對象2都沒有被堆外的變量引用,而是被對方互相引用,這時他們雖然沒有用處了,但是引用計數(shù)器的值仍然是1,無法判斷他們是死對象,垃圾回收器也就無法回收。

img

2、可達性分析算法

了解可達性分析算法之前先了解一個概念——GC Roots,垃圾收集的起點,可以作為GC Roots的有虛擬機棧中本地變量表中引用的對象、方法區(qū)中靜態(tài)屬性引用的對象、方法區(qū)中常量引用的對象、本地方法棧中JNI(Native方法)引用的對象。 當一個對象到GC Roots沒有任何引用鏈相連(GC Roots到這個對象不可達)時,就說明此對象是不可用的,是死對象。如下圖:object1、object2、object3、object4和GC Roots之間有可達路徑,這些對象不會被回收,但object5、object6、object7到GC Roots之間沒有可達路徑,這些對象就是死對象。

img

上面被判定為非存活的死對象(object5、object6、object7)并不是必死無疑,還有挽救的余地。進行可達性分析后對象和GC Roots之間沒有引用鏈相連時,對象將會被進行一次標記,接著會判斷如果對象沒有覆蓋Object的finalize()方法或者finalize()方法已經(jīng)被虛擬機調(diào)用過,那么它們就會清除;如果對象覆蓋了finalize()方法且還沒有被調(diào)用,則會執(zhí)行finalize()方法中的內(nèi)容,所以在finalize()方法中如果重新與GC Roots引用鏈上的對象關(guān)聯(lián)就可以拯救自己。當然,實際中一般不會這么做。

GC算法

接下來講GC的算法,主要有標記-清除算法、復制算法、標記-整理算法、分代收集算法。

1、標記-清除算法

最基礎的收集算法是“標記-清除”(Mark-Sweep)算法,分兩個階段:首先標記出所有需要回收的對象,在標記完成后統(tǒng)一回收所有被標記的對象。

優(yōu)點:不需要進行對象的移動,并且僅對不存活的對象進行處理,在存活對象比較多的情況極為有效。

不足:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能導致以后在程序運行過程需要分配較大對象時,無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一個的垃圾收集動作。

下面兩張圖從兩個角度闡明了標記-清楚算法:

img
img

2、復制算法

為了解決效率問題,一種稱為復制(Copying)的收集算法出現(xiàn)了,它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊內(nèi)存用完了,就將還存活著的對象復制到另外一塊上,然后再把已經(jīng)使用過的內(nèi)存空間一次清理掉。這樣使得每次都是對整個半?yún)^(qū)進行內(nèi)存回收,內(nèi)存分配時也就不用考慮內(nèi)存碎片等復雜情況,只要移動堆頂指針,按順序分配內(nèi)存即可,實現(xiàn)簡單,運行高效。代價是內(nèi)存縮小為原來的一半。

復制算法過程如下面兩張圖表示:

img
img

商業(yè)虛擬機用這個回收算法來回收新生代。IBM研究表明98%的對象是“朝生夕死“,不需要按照1-1的比例來劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的”Eden“空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活的對象一次性復制到另外一個Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。Hotspot虛擬機默認Eden和Survivor的比例是8-1.即每次可用整個新生代的90%, 只有一個survivor,即1/10被”浪費“。當然,98%的對象回收只是一般場景下的數(shù)據(jù),我們沒有辦法保證每次回收都只有不多于10%的對象存活,當Survivor空間不夠時,需要依賴其他內(nèi)存(老年代)進行分配擔保(Handle Promotion).

如果另外一塊survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象時,這些對象將直接通過分配擔保機制進入老年代。

下面大概介紹一下這個eden survivor復制的過程。

Eden Space字面意思是伊甸園,對象被創(chuàng)建的時候首先放到這個區(qū)域,進行垃圾回收后,不能被回收的對象被放入到空的survivor區(qū)域。

Survivor Space幸存者區(qū),用于保存在eden space內(nèi)存區(qū)域中經(jīng)過垃圾回收后沒有被回收的對象。Survivor有兩個,分別為To Survivor、 From Survivor,這個兩個區(qū)域的空間大小是一樣的。執(zhí)行垃圾回收的時候Eden區(qū)域不能被回收的對象被放入到空的survivor(也就是To Survivor,同時Eden區(qū)域的內(nèi)存會在垃圾回收的過程中全部釋放),另一個survivor(即From Survivor)里不能被回收的對象也會被放入這個survivor(即To Survivor),然后To Survivor 和 From Survivor的標記會互換,始終保證一個survivor是空的。

為啥需要兩個survivor?因為需要一個完整的空間來復制過來。當滿的時候晉升。每次都往標記為to的里面放,然后互換,這時from已經(jīng)被清空,可以當作to了。

3、標記-整理算法

復制收集算法在對象成活率較高時就要進行較多的復制操作,效率將會變低。更關(guān)鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內(nèi)存中所有對象都100%存活的極端情況,所以,老年代一般不能直接選用這種算法。

根據(jù)老年代的特點,有人提出一種”標記-整理“Mark-Compact算法,標記過程仍然和標記-清除一樣,但后續(xù)步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理端邊界以外的內(nèi)存。

下面兩張圖講了這個算法的過程:

img
img

4、分代收集算法

當前商業(yè)虛擬機的垃圾收集都采用”分代收集“(Generational Collection)算法,這種算法根據(jù)對象存活周期的不同將內(nèi)存劃分為幾塊。一般把Java堆分為新生代和老年代,這樣就可以根據(jù)各個年代的特點采用最適當?shù)氖占惴?。在新生代,每次垃圾收集時都發(fā)現(xiàn)大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。而老年代中因為對象存活率較高,沒有額外的空間對它進行分配擔保,就必須使用”標記-清理“和”標記-整理“算法來進行回收。

這種算法就是我們在前面JVM垃圾回收綜述中講述的內(nèi)容。其本質(zhì)是更為靈活的使用”標記-清理“和”標記-整理“算法。

常見的GC回收器

現(xiàn)在常見的垃圾收集器有如下幾種

新生代收集器:Serial、ParNew、Parallel Scavenge

老年代收集器:Serial Old、CMS、Parallel Old

堆內(nèi)存垃圾收集器:G1

如圖所示:

img

0、垃圾收集時間

當程序運行時,各種數(shù)據(jù)、對象、線程、內(nèi)存等都時刻在發(fā)生變化,當下達垃圾收集命令后垃圾收集器并不會立刻執(zhí)行垃圾收集。為了搞明白垃圾收集器的工作原理,我們需要講兩個名詞:安全點(safepoint)和安全區(qū)(safe region)。

安全點:從線程角度看,安全點可以理解為是在代碼執(zhí)行過程中的一些特殊位置,當線程執(zhí)行到安全點的時候,說明虛擬機當前的狀態(tài)是安全的,如果有需要,可以在這里暫停用戶線程。當垃圾收集時,如果需要暫停當前的用戶線程,但用戶線程當時沒在安全點上,則應該等待這些線程執(zhí)行到安全點再暫停。

安全區(qū):安全點是相對于運行中的線程來說的,對于如sleep或blocked等狀態(tài)的線程,收集器不會等待這些線程被分配CPU時間,這時候只要線程處于安全區(qū)中,就可以算是安全的。安全區(qū)就是在一段代碼片段中,引用關(guān)系不會發(fā)生變化,可以看作是被擴展、拉長了的安全點。

GC過程一定會發(fā)生STW(Stop The World),而一旦發(fā)生STW必然會影響用戶使用,所以GC的發(fā)展都是在圍繞減少STW時間這一目的。

1、Serial 收集器

Serial是一款用于新生代的單線程收集器,采用復制算法進行垃圾收集。Serial進行垃圾收集時,不僅只用一條線程執(zhí)行垃圾收集工作,它在收集的同時,所有的用戶線程必須暫停(Stop The World)。 如下是Serial收集器和Serial Old收集器結(jié)合進行垃圾收集的示意圖,當用戶線程都執(zhí)行到安全點時,所有線程暫停執(zhí)行,Serial收集器以單線程,采用復制算法進行垃圾收集工作,收集完之后,用戶線程繼續(xù)開始執(zhí)行。

img

適用場景:Client模式(桌面應用);單核服務器??梢杂?XX:+UserSerialGC來選擇Serial作為新生代收集器。

2、ParNew 收集器

ParNew就是一個Serial的多線程版本,其它與Serial并無區(qū)別。ParNew在單核CPU環(huán)境并不會比Serial收集器達到更好的效果,它默認開啟的收集線程數(shù)和CPU數(shù)量一致,可以通過-XX:ParallelGCThreads來設置垃圾收集的線程數(shù)。 如下是ParNew收集器和Serial Old收集器結(jié)合進行垃圾收集的示意圖,當用戶線程都執(zhí)行到安全點時,所有線程暫停執(zhí)行,ParNew收集器以多線程,采用復制算法進行垃圾收集工作,收集完之后,用戶線程繼續(xù)開始執(zhí)行。

img

適用場景:多核服務器;與CMS收集器搭配使用。當使用-XX:+UserConcMarkSweepGC來選擇CMS作為老年代收集器時,新生代收集器默認就是ParNew,也可以用-XX:+UseParNewGC來指定使用ParNew作為新生代收集器。

3、Parallel Scavenge 收集器

Parallel Scavenge也是一款用于新生代的多線程收集器,與ParNew的不同之處是,ParNew的目標是盡可能縮短垃圾收集時用戶線程的停頓時間,Parallel Scavenge的目標是達到一個可控制的吞吐量。吞吐量就是CPU執(zhí)行用戶線程的的時間與CPU執(zhí)行總時間的比值【吞吐量=運行用戶代代碼時間/(運行用戶代碼時間+垃圾收集時間)】,比如虛擬機一共運行了100分鐘,其中垃圾收集花費了1分鐘,那吞吐量就是99% 。比如下面兩個場景,垃圾收集器每100秒收集一次,每次停頓10秒,和垃圾收集器每50秒收集一次,每次停頓時間7秒,雖然后者每次停頓時間變短了,但是總體吞吐量變低了,CPU總體利用率變低了。

收集頻率 每次停頓時間 吞吐量
每100秒收集一次 10秒 91%
每50秒收集一次 7秒 88%

可以通過-XX:MaxGCPauseMillis來設置收集器盡可能在多長時間內(nèi)完成內(nèi)存回收,可以通過-XX:GCTimeRatio來精確控制吞吐量。

如下是Parallel收集器和Parallel Old收集器結(jié)合進行垃圾收集的示意圖,在新生代,當用戶線程都執(zhí)行到安全點時,所有線程暫停執(zhí)行,ParNew收集器以多線程,采用復制算法進行垃圾收集工作,收集完之后,用戶線程繼續(xù)開始執(zhí)行;在老年代,當用戶線程都執(zhí)行到安全點時,所有線程暫停執(zhí)行,Parallel Old收集器以多線程,采用標記整理算法進行垃圾收集工作。

img

適用場景:注重吞吐量,高效利用CPU,需要高效運算且不需要太多交互??梢允褂?XX:+UseParallelGC來選擇Parallel Scavenge作為新生代收集器,jdk7、jdk8默認使用Parallel Scavenge作為新生代收集器。

4、Serial Old收集器

Serial Old收集器是Serial的老年代版本,同樣是一個單線程收集器,采用標記-整理算法。

如下圖是Serial收集器和Serial Old收集器結(jié)合進行垃圾收集的示意圖:

img

適用場景:Client模式(桌面應用);單核服務器;與Parallel Scavenge收集器搭配;作為CMS收集器的后備預案。

5、CMS(Concurrent Mark Sweep) 收集器

CMS收集器是一種以最短回收停頓時間為目標的收集器,以“最短用戶線程停頓時間”著稱。整個垃圾收集過程分為4個步驟:

  1. 初始標記:標記一下GC Roots能直接關(guān)聯(lián)到的對象,速度較快

  2. 并發(fā)標記:進行GC Roots Tracing,標記出全部的垃圾對象,耗時較長

  3. 重新標記:修正并發(fā)標記階段引用戶程序繼續(xù)運行而導致變化的對象的標記記錄,耗時較短

  4. 并發(fā)清除:用標記-清除算法清除垃圾對象,耗時較長

整個過程耗時最長的并發(fā)標記和并發(fā)清除都是和用戶線程一起工作,所以從總體上來說,CMS收集器垃圾收集可以看做是和用戶線程并發(fā)執(zhí)行的。

img

CMS收集器也存在一些缺點:

  • 對CPU資源敏感:默認分配的垃圾收集線程數(shù)為(CPU數(shù)+3)/4,隨著CPU數(shù)量下降,占用CPU資源越多,吞吐量越小

  • 無法處理浮動垃圾:在并發(fā)清理階段,由于用戶線程還在運行,還會不斷產(chǎn)生新的垃圾,CMS收集器無法在當次收集中清除這部分垃圾。同時由于在垃圾收集階段用戶線程也在并發(fā)執(zhí)行,CMS收集器不能像其他收集器那樣等老年代被填滿時再進行收集,需要預留一部分空間提供用戶線程運行使用。當CMS運行時,預留的內(nèi)存空間無法滿足用戶線程的需要,就會出現(xiàn)“Concurrent Mode Failure”的錯誤,這時將會啟動后備預案,臨時用Serial Old來重新進行老年代的垃圾收集。

  • 因為CMS是基于標記-清除算法,所以垃圾回收后會產(chǎn)生空間碎片,可以通過-XX:UserCMSCompactAtFullCollection開啟碎片整理(默認開啟),在CMS進行Full GC之前,會進行內(nèi)存碎片的整理。還可以用-XX:CMSFullGCsBeforeCompaction設置執(zhí)行多少次不壓縮(不進行碎片整理)的Full GC之后,跟著來一次帶壓縮(碎片整理)的Full GC。

適用場景:重視服務器響應速度,要求系統(tǒng)停頓時間最短??梢允褂?XX:+UserConMarkSweepGC來選擇CMS作為老年代收集器。

6、Parallel Old 收集器

Parallel Old收集器是Parallel Scavenge的老年代版本,是一個多線程收集器,采用標記-整理算法??梢耘cParallel Scavenge收集器搭配,可以充分利用多核CPU的計算能力。如Parallel Scavenge中的兩個垃圾收集器的搭配使用圖。

適用場景:與Parallel Scavenge收集器搭配使用;注重吞吐量。jdk7、jdk8默認使用該收集器作為老年代收集器,使用 -XX:+UseParallelOldGC來指定使用Paralle Old收集器。

7、G1 收集器

上述的一些GC收集器通過并行與并發(fā)已經(jīng)極大的減少了STW的時間,但是STW的時間還是會因為各種原因不可控,而G1提供的一個最大功能就是可控的STW時間。

G1通過把Java堆分成大小相等的多個獨立區(qū)域,回收時計算出每個區(qū)域回收所獲得的空間以及所需時間的經(jīng)驗值,根據(jù)記錄兩個值來判斷哪個區(qū)域最具有回收價值,所以叫Garbage First(垃圾優(yōu)先)。

這里有幾個重要的概念:

  • Region(區(qū)域):G1采用了分區(qū)(Region)的思路,將整個堆空間分成若干個大小相等的內(nèi)存區(qū)域,每次分配對象空間將逐段地使用內(nèi)存。因此,在堆的使用上,G1并不要求對象的存儲一定是物理上連續(xù)的,只要邏輯上連續(xù)即可;每個分區(qū)也不會確定地為某個代服務,可以按需在年輕代和老年代之間切換。啟動時可以通過參數(shù)-XX:G1HeapRegionSize=n可指定分區(qū)大小(1MB~32MB,且必須是2的冪),默認將整堆劃分為2048個分區(qū)。

  • Card(卡片):在每個分區(qū)內(nèi)部又被分成了若干個大小為512 Byte卡片(Card),標識堆內(nèi)存最小可用粒度所有分區(qū)的卡片將會記錄在全局卡片表(Global Card Table)中,分配的對象會占用物理上連續(xù)的若干個卡片,當查找對分區(qū)內(nèi)對象的引用時便可通過記錄卡片來查找該引用對象(見RSet)。每次對內(nèi)存的回收,都是對指定分區(qū)的卡片進行處理。

  • CSet(收集集合):GC過程記錄的可被回收的Region的集合。在CSet中存活的數(shù)據(jù)會在GC過程中被移動到另一個可用分區(qū),CSet中的分區(qū)可以來自eden空間、survivor空間、或者老年代。

  • RSet(Remembered Set 記憶集合):記錄了其他Region中的對象引用本Region中對象的關(guān)系,屬于points-into結(jié)構(gòu) (誰引用了我的對象)。作用是不需要掃描整個堆找到誰引用了當前分區(qū)中的對象,只需要掃描RSet即可。

  • Humongous regions:用來存放大于標準的Region內(nèi)存50%的大對象區(qū)域,如果有些對象大于整個Region就會去找連續(xù)的Region保存,如果沒有就會觸發(fā)GC。

G1收集器與之前的收集器最大的不同就在于堆內(nèi)存的劃分,之前的收集器只區(qū)分新生代與老年代,而G1收集器則是把堆內(nèi)存劃分成多個獨立的Region。

img

在上圖中G1的Java堆中每個Region都有一個身份,每個Region有可能是eden、survivor、old,但是他們的身份僅僅是邏輯上的,是可以變化的,G1可以根據(jù)情況動態(tài)的調(diào)整各種Region的數(shù)量,通過控制回收的Region數(shù)量來控制STW的時間,以達到STW時間的可控制。

雖然G1收集器把Java堆化整為零成一個個Region,但是也不會進行所有Region進行收集,G1也分成了兩種收集模式,兩種模式如下:

Young GC: CSet就是所有年輕代里面的Region;

Mixed GC: CSet是所有年輕代里的Region加上在全局并發(fā)標記階段標記出來的收益高的老年代Region;

Young GC過程:

階段1:根掃描,靜態(tài)和本地對象被掃描;

階段2:更新RS,處理dirty card隊列更新RS;

階段3:處理RS,檢測從年輕代指向老年代的對象;

階段4:對象拷貝,拷貝存活的對象到survivorl/old區(qū)域;

階段5:處理引用隊列,軟引用,弱引用,虛引用處理;

Mixed GC過程:

1、全局并發(fā)標記(global concurrent marking)

2、拷貝存活對象(evacuation)

全局并發(fā)標記包括5個步驟:

1、初始標記(initial mark,STW):標記了從GCRoot開始直接可達的對象。

2、根區(qū)域掃描(root region scan):G1 GC 在初始標記的存活區(qū)掃描對老年代的引用,并標記被引用的對象。該階段與應用程序(非 STW)同時運行,并且只有完成該階段后,才能開始下一次 STW 年輕代垃圾回收。

3、并發(fā)標記(Concurrent Marking):G1 GC 在整個堆中查找可訪問的(存活的)對象。該階段與應用程序同時運行,可以被 STW 年輕代垃圾回收中斷。

4、重新標記(Remark,STW):該階段是 STW 回收,幫助完成標記周期。G1 GC 清空 SATB 緩沖區(qū),跟蹤未被訪問的存活對象,并執(zhí)行引用處理。

5、清除垃圾(Cleanup):在這個最后階段,G1 GC 執(zhí)行統(tǒng)計和 RSet 凈化的 STW 操作。在統(tǒng)計期間,G1 GC 會識別完全空閑的區(qū)域和可供進行混合垃圾回收的區(qū)域。清理階段在將空白區(qū)域重置并返回到空閑列表時為部分并發(fā)。

適用場景:要求盡可能可控GC停頓時間;內(nèi)存占用較大的應用。可以用-XX:+UseG1GC使用G1收集器,jdk9默認使用G1收集器。

GC日志

每一種回收器的日志格式都是由其自身的實現(xiàn)決定的,換而言之,每種回收器的日志格式都可以不一樣。但虛擬機設計者為了方便用戶閱讀,將各個回收器的日志都維持一定的共性。

GC日志是學GC調(diào)優(yōu)之前的必備前置條件,所以我們必須學會。下面放兩張網(wǎng)圖,大家可以從中看到日志的每個節(jié)點:

young gc 日志:

img

Full GC日志:

img

文章首發(fā)于:
九神帶你入門JVM(下)

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

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

  • 概述 本篇較長,九神帶你從0入門JVM,全文包括包括JVM的分類、JVM垃圾回收綜述、JVM的內(nèi)存模型(Java ...
    九神說編程閱讀 481評論 0 1
  • 前言 在這篇文章介紹并記錄關(guān)于JVM內(nèi)存結(jié)構(gòu)的簡單應用學習。基于JDK8。 1.JVM內(nèi)存結(jié)構(gòu) 1.1運行時數(shù)據(jù)區(qū)...
    西茶閱讀 471評論 0 0
  • 前言 只有光頭才能變強 JVM在準備面試的時候就有看了,一直沒時間寫筆記。現(xiàn)在到了一家公司實習,閑的時候就寫寫,刷...
    Java3y閱讀 25,703評論 11 129
  • 久違的晴天,家長會。 家長大會開好到教室時,離放學已經(jīng)沒多少時間了。班主任說已經(jīng)安排了三個家長分享經(jīng)驗。 放學鈴聲...
    飄雪兒5閱讀 7,787評論 16 22
  • 今天感恩節(jié)哎,感謝一直在我身邊的親朋好友。感恩相遇!感恩不離不棄。 中午開了第一次的黨會,身份的轉(zhuǎn)變要...
    余生動聽閱讀 10,798評論 0 11

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