九神帶你入門JVM(下)

咱們接着上面一篇繼續學習JVM的基本知識。算法

對象存活判斷

上篇中咱們介紹過JVM垃圾回收綜述中說過一次垃圾回收以後會有一些對象存活。這節咱們介紹兩個判斷對象存活的算法。安全

判斷對象存活有引用計數算法和可達性分析算法。服務器

一、引用計數算法多線程

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

這種方法看起來很是簡單,但目前許多主流的虛擬機都沒有選用這種算法來管理內存,緣由就是當某些對象之間互相引用時,沒法判斷出這些對象是否已死。以下圖,對象1和對象2都沒有被堆外的變量引用,而是被對方互相引用,這時他們雖然沒有用處了,可是引用計數器的值仍然是1,沒法判斷他們是死對象,垃圾回收器也就沒法回收。學習

img

二、可達性分析算法spa

瞭解可達性分析算法以前先了解一個概念——GC Roots,垃圾收集的起點,能夠做爲GC Roots的有虛擬機棧中本地變量表中引用的對象、方法區中靜態屬性引用的對象、方法區中常量引用的對象、本地方法棧中JNI(Native方法)引用的對象。 當一個對象到GC Roots沒有任何引用鏈相連(GC Roots到這個對象不可達)時,就說明此對象是不可用的,是死對象。以下圖:object一、object二、object三、object4和GC Roots之間有可達路徑,這些對象不會被回收,但object五、object六、object7到GC Roots之間沒有可達路徑,這些對象就是死對象。線程

img

上面被斷定爲非存活的死對象(object五、object六、object7)並非必死無疑,還有挽救的餘地。進行可達性分析後對象和GC Roots之間沒有引用鏈相連時,對象將會被進行一次標記,接着會判斷若是對象沒有覆蓋Object的finalize()方法或者finalize()方法已經被虛擬機調用過,那麼它們就會清除;若是對象覆蓋了finalize()方法且尚未被調用,則會執行finalize()方法中的內容,因此在finalize()方法中若是從新與GC Roots引用鏈上的對象關聯就能夠拯救本身。固然,實際中通常不會這麼作。設計

GC算法

接下來說GC的算法,主要有標記-清除算法、複製算法、標記-整理算法、分代收集算法。3d

一、標記-清除算法

最基礎的收集算法是「標記-清除」(Mark-Sweep)算法,分兩個階段:首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象。

優勢:不須要進行對象的移動,而且僅對不存活的對象進行處理,在存活對象比較多的狀況極爲有效。

不足:一個是效率問題,標記和清除兩個過程的效率都不高;另外一個是空間問題,標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能致使之後在程序運行過程須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發另外一個的垃圾收集動做。

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

img

img

二、複製算法

爲了解決效率問題,一種稱爲複製(Copying)的收集算法出現了,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊內存用完了,就將還存活着的對象複製到另一塊上,而後再把已經使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。代價是內存縮小爲原來的一半。

複製算法過程以下面兩張圖表示:

img

img

商業虛擬機用這個回收算法來回收新生代。IBM研究代表98%的對象是「朝生夕死「,不須要按照1-1的比例來劃份內存空間,而是將內存分爲一塊較大的」Eden「空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活的對象一次性複製到另一個Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。Hotspot虛擬機默認Eden和Survivor的比例是8-1.即每次可用整個新生代的90%, 只有一個survivor,即1/10被」浪費「。固然,98%的對象回收只是通常場景下的數據,咱們沒有辦法保證每次回收都只有很少於10%的對象存活,當Survivor空間不夠時,須要依賴其餘內存(老年代)進行分配擔保(Handle Promotion).

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

下面大概介紹一下這個eden survivor複製的過程。

Eden Space字面意思是伊甸園,對象被建立的時候首先放到這個區域,進行垃圾回收後,不能被回收的對象被放入到空的survivor區域。

Survivor Space倖存者區,用於保存在eden space內存區域中通過垃圾回收後沒有被回收的對象。Survivor有兩個,分別爲To Survivor、 From Survivor,這個兩個區域的空間大小是同樣的。執行垃圾回收的時候Eden區域不能被回收的對象被放入到空的survivor(也就是To Survivor,同時Eden區域的內存會在垃圾回收的過程當中所有釋放),另外一個survivor(即From Survivor)裏不能被回收的對象也會被放入這個survivor(即To Survivor),而後To Survivor 和 From Survivor的標記會互換,始終保證一個survivor是空的。

爲啥須要兩個survivor?由於須要一個完整的空間來複制過來。當滿的時候晉升。每次都往標記爲to的裏面放,而後互換,這時from已經被清空,能夠看成to了。

三、標記-整理算法

複製收集算法在對象成活率較高時就要進行較多的複製操做,效率將會變低。更關鍵的是,若是不想浪費50%的空間,就須要有額外的空間進行分配擔保,以應對被使用的內存中全部對象都100%存活的極端狀況,因此,老年代通常不能直接選用這種算法。

根據老年代的特色,有人提出一種」標記-整理「Mark-Compact算法,標記過程仍然和標記-清除同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理端邊界之外的內存。

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

img

img

四、分代收集算法

當前商業虛擬機的垃圾收集都採用」分代收集「(Generational Collection)算法,這種算法根據對象存活週期的不一樣將內存劃分爲幾塊。通常把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。在新生代,每次垃圾收集時都發現大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象存活率較高,沒有額外的空間對它進行分配擔保,就必須使用」標記-清理「和」標記-整理「算法來進行回收。

這種算法就是咱們在前面JVM垃圾回收綜述中講述的內容。其本質是更爲靈活的使用」標記-清理「和」標記-整理「算法。

常見的GC回收器

如今常見的垃圾收集器有以下幾種

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

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

堆內存垃圾收集器:G1

如圖所示:

img

0、垃圾收集時間

當程序運行時,各類數據、對象、線程、內存等都時刻在發生變化,當下達垃圾收集命令後垃圾收集器並不會馬上執行垃圾收集。爲了搞明白垃圾收集器的工做原理,咱們須要講兩個名詞:安全點(safepoint)和安全區(safe region)。

安全點:從線程角度看,安全點能夠理解爲是在代碼執行過程當中的一些特殊位置,當線程執行到安全點的時候,說明虛擬機當前的狀態是安全的,若是有須要,能夠在這裏暫停用戶線程。當垃圾收集時,若是須要暫停當前的用戶線程,但用戶線程當時沒在安全點上,則應該等待這些線程執行到安全點再暫停。

安全區:安全點是相對於運行中的線程來講的,對於如sleep或blocked等狀態的線程,收集器不會等待這些線程被分配CPU時間,這時候只要線程處於安全區中,就能夠算是安全的。安全區就是在一段代碼片斷中,引用關係不會發生變化,能夠看做是被擴展、拉長了的安全點。

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

一、Serial 收集器

Serial是一款用於新生代的單線程收集器,採用複製算法進行垃圾收集。Serial進行垃圾收集時,不只只用一條線程執行垃圾收集工做,它在收集的同時,全部的用戶線程必須暫停(Stop The World)。 以下是Serial收集器和Serial Old收集器結合進行垃圾收集的示意圖,當用戶線程都執行到安全點時,全部線程暫停執行,Serial收集器以單線程,採用複製算法進行垃圾收集工做,收集完以後,用戶線程繼續開始執行。

img

適用場景:Client模式(桌面應用);單核服務器。能夠用-XX:+UserSerialGC來選擇Serial做爲新生代收集器。

二、ParNew 收集器

ParNew就是一個Serial的多線程版本,其它與Serial並沒有區別。ParNew在單核CPU環境並不會比Serial收集器達到更好的效果,它默認開啓的收集線程數和CPU數量一致,能夠經過-XX:ParallelGCThreads來設置垃圾收集的線程數。 以下是ParNew收集器和Serial Old收集器結合進行垃圾收集的示意圖,當用戶線程都執行到安全點時,全部線程暫停執行,ParNew收集器以多線程,採用複製算法進行垃圾收集工做,收集完以後,用戶線程繼續開始執行。

img

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

三、Parallel Scavenge 收集器

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

收集頻率

每次停頓時間

吞吐量

每100秒收集一次

10秒

91%

每50秒收集一次

7秒

88%

能夠經過-XX:MaxGCPauseMillis來設置收集器儘量在多長時間內完成內存回收,能夠經過-XX:GCTimeRatio來精確控制吞吐量。

以下是Parallel收集器和Parallel Old收集器結合進行垃圾收集的示意圖,在新生代,當用戶線程都執行到安全點時,全部線程暫停執行,ParNew收集器以多線程,採用複製算法進行垃圾收集工做,收集完以後,用戶線程繼續開始執行;在老年代,當用戶線程都執行到安全點時,全部線程暫停執行,Parallel Old收集器以多線程,採用標記整理算法進行垃圾收集工做。

img

適用場景:注重吞吐量,高效利用CPU,須要高效運算且不須要太多交互。可使用-XX:+UseParallelGC來選擇Parallel Scavenge做爲新生代收集器,jdk七、jdk8默認使用Parallel Scavenge做爲新生代收集器。

四、Serial Old收集器

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

以下圖是Serial收集器和Serial Old收集器結合進行垃圾收集的示意圖:

img

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

五、CMS(Concurrent Mark Sweep) 收集器

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

  1. 初始標記:標記一下GC Roots能直接關聯到的對象,速度較快
  2. 併發標記:進行GC Roots Tracing,標記出所有的垃圾對象,耗時較長
  3. 從新標記:修正併發標記階段引用戶程序繼續運行而致使變化的對象的標記記錄,耗時較短
  4. 併發清除:用標記-清除算法清除垃圾對象,耗時較長

整個過程耗時最長的併發標記和併發清除都是和用戶線程一塊兒工做,因此從整體上來講,CMS收集器垃圾收集能夠看作是和用戶線程併發執行的。

img

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

  • 對CPU資源敏感:默認分配的垃圾收集線程數爲(CPU數+3)/4,隨着CPU數量降低,佔用CPU資源越多,吞吐量越小
  • 沒法處理浮動垃圾:在併發清理階段,因爲用戶線程還在運行,還會不斷產生新的垃圾,CMS收集器沒法在當次收集中清除這部分垃圾。同時因爲在垃圾收集階段用戶線程也在併發執行,CMS收集器不能像其餘收集器那樣等老年代被填滿時再進行收集,須要預留一部分空間提供用戶線程運行使用。當CMS運行時,預留的內存空間沒法知足用戶線程的須要,就會出現「Concurrent Mode Failure」的錯誤,這時將會啓動後備預案,臨時用Serial Old來從新進行老年代的垃圾收集。
  • 由於CMS是基於標記-清除算法,因此垃圾回收後會產生空間碎片,能夠經過-XX:UserCMSCompactAtFullCollection開啓碎片整理(默認開啓),在CMS進行Full GC以前,會進行內存碎片的整理。還能夠用-XX:CMSFullGCsBeforeCompaction設置執行多少次不壓縮(不進行碎片整理)的Full GC以後,跟着來一次帶壓縮(碎片整理)的Full GC。

適用場景:重視服務器響應速度,要求系統停頓時間最短。可使用-XX:+UserConMarkSweepGC來選擇CMS做爲老年代收集器。

六、Parallel Old 收集器

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

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

七、G1 收集器

上述的一些GC收集器經過並行與併發已經極大的減小了STW的時間,可是STW的時間仍是會由於各類緣由不可控,而G1提供的一個最大功能就是可控的STW時間。

G1經過把Java堆分紅大小相等的多個獨立區域,回收時計算出每一個區域回收所得到的空間以及所需時間的經驗值,根據記錄兩個值來判斷哪一個區域最具備回收價值,因此叫Garbage First(垃圾優先)。

這裏有幾個重要的概念:

  • Region(區域):G1採用了分區(Region)的思路,將整個堆空間分紅若干個大小相等的內存區域,每次分配對象空間將逐段地使用內存。所以,在堆的使用上,G1並不要求對象的存儲必定是物理上連續的,只要邏輯上連續便可;每一個分區也不會肯定地爲某個代服務,能夠按需在年輕代和老年代之間切換。啓動時能夠經過參數-XX:G1HeapRegionSize=n可指定分區大小(1MB~32MB,且必須是2的冪),默認將整堆劃分爲2048個分區。
  • Card(卡片):在每一個分區內部又被分紅了若干個大小爲512 Byte卡片(Card),標識堆內存最小可用粒度全部分區的卡片將會記錄在全局卡片表(Global Card Table)中,分配的對象會佔用物理上連續的若干個卡片,當查找對分區內對象的引用時即可經過記錄卡片來查找該引用對象(見RSet)。每次對內存的回收,都是對指定分區的卡片進行處理。
  • CSet(收集集合):GC過程記錄的可被回收的Region的集合。在CSet中存活的數據會在GC過程當中被移動到另外一個可用分區,CSet中的分區能夠來自eden空間、survivor空間、或者老年代。
  • RSet(Remembered Set 記憶集合):記錄了其餘Region中的對象引用本Region中對象的關係,屬於points-into結構 (誰引用了個人對象)。做用是不須要掃描整個堆找到誰引用了當前分區中的對象,只須要掃描RSet便可。
  • Humongous regions:用來存放大於標準的Region內存50%的大對象區域,若是有些對象大於整個Region就會去找連續的Region保存,若是沒有就會觸發GC。

G1收集器與以前的收集器最大的不一樣就在於堆內存的劃分,以前的收集器只區分新生代與老年代,而G1收集器則是把堆內存劃分紅多個獨立的Region。

img

在上圖中G1的Java堆中每一個Region都有一個身份,每一個Region有多是eden、survivor、old,可是他們的身份僅僅是邏輯上的,是能夠變化的,G1能夠根據狀況動態的調整各類Region的數量,經過控制回收的Region數量來控制STW的時間,以達到STW時間的可控制。

雖然G1收集器把Java堆化整爲零成一個個Region,可是也不會進行全部Region進行收集,G1也分紅了兩種收集模式,兩種模式以下:

Young GC: CSet就是全部年輕代裏面的Region;

Mixed GC: CSet是全部年輕代裏的Region加上在全局併發標記階段標記出來的收益高的老年代Region;

Young GC過程:

階段1:根掃描,靜態和本地對象被掃描;

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

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

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

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

Mixed GC過程:

一、全局併發標記(global concurrent marking)

二、拷貝存活對象(evacuation)

全局併發標記包括5個步驟:

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

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

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

四、從新標記(Remark,STW):該階段是 STW 回收,幫助完成標記週期。G1 GC 清空 SATB 緩衝區,跟蹤未被訪問的存活對象,並執行引用處理。

五、清除垃圾(Cleanup):在這個最後階段,G1 GC 執行統計和 RSet 淨化的 STW 操做。在統計期間,G1 GC 會識別徹底空閒的區域和可供進行混合垃圾回收的區域。清理階段在將空白區域重置並返回到空閒列表時爲部分併發。

適用場景:要求儘量可控GC停頓時間;內存佔用較大的應用。能夠用-XX:+UseG1GC使用G1收集器,jdk9默認使用G1收集器。

GC日誌

每一種回收器的日誌格式都是由其自身的實現決定的,換而言之,每種回收器的日誌格式均可以不同。但虛擬機設計者爲了方便用戶閱讀,將各個回收器的日誌都維持必定的共性。

GC日誌是學GC調優以前的必備前置條件,因此咱們必須學會。下面放兩張網圖,你們能夠從中看到日誌的每一個節點:

young gc 日誌:

img

Full GC日誌:

img

文章首發於:
九神帶你入門JVM(下)