【轉】一文看懂JVM內存佈局及GC原理

做者簡介html

楊俊明,攜程雲客服平臺研發部軟件技術專家。從事IT行業10餘年,騰訊雲+社區、阿里雲棲社區、華爲雲社區認證專家。近年來主要研究分佈式架構、微服務、Java技術等方向。前端

 

java的內存佈局以及GC原理」是java開發人員繞不開的話題,也是面試中常見的高頻問題之一。java

 

java發展歷史上出現過不少垃圾回收器,各有各的適應場景,不少網上的舊文章已經跟不上的變化。本文詳細介紹了java的內存佈局以及各類垃圾回收器的原理(包括的ZGC),但願閱讀完後,你們對這方面的知識再也不陌生,有所收穫,同時也歡迎你們留言討論。ios

 

1、JVM運行時內存佈局c++

按java 8虛擬機規範的原始表達:(jvm)Run-Time Data Areas, 暫時翻譯爲「jvm運行時內存佈局」。git

 

從概念上大體分爲6個(邏輯)區域,參考下圖。注:Method Area中還有一個常量池區,圖中未明確標出。github

 

這6塊區域按是否被線程共享,能夠分爲兩大類:web

 

一類是每一個線程所獨享的:面試

1)PC Register:也稱爲程序計數器, 記錄每一個線程當前執行的指令信。eg:當前執行到哪一條指令,下一條該取哪條指令。算法

 

2)JVM Stack:也稱爲虛擬機棧,記錄每一個棧幀(Frame)中的局部變量、方法返回地址等。注:這裏出現了一個新名詞「棧幀」,它的結構以下:

 

線程中每次有方法調用時,會建立Frame,方法調用結束時Frame銷燬。

 

3)Native Method Stack:本地(原生)方法棧,顧名思義就是調用操做系統原生本地方法時,所須要的內存區域。

 

上述3類區域,生命週期與Thread相同,即:線程建立時,相應的區域分配內存,線程銷燬時,釋放相應內存。

 

另外一類是全部線程共享的:

1)Heap:即鼎鼎大名的堆內存區,也是GC垃圾回收的主站場,用於存放類的實例對象及Arrays實例等。

 

2)Method Area:方法區,主要存放類結構、類成員定義,static靜態成員等。

 

3)Runtime Constant Pool:運行時常量池,好比:字符串,int -128~127範圍的值等,它是Method Area中的一部分。

 

Heap、Method Area 都是在虛擬機啓動時建立,虛擬機退出時釋放。

 

注:Method Area 區,虛擬機規範只是說必需要有,可是具體怎麼實現(好比:是否須要垃圾回收? ),交給具體的JVM實現去決定,邏輯上講,視爲Heap區的一部分。因此,若是你看見相似下面的圖,也不要以爲畫錯了。

 

上述6個區域,除了PC Register區不會拋出StackOverflowError或OutOfMemoryError ,其它5個區域,當請求分配的內存不足時,均會拋出OutOfMemoryError(即:OOM),其中thread獨立的JVM Stack區及Native Method Stack區還會拋出StackOverflowError。

 

最後,還有一類不受JVM虛擬機管控的內存區,這裏也提一下,即:堆外內存。

 

能夠經過Unsafe和NIO包下的DirectByteBuffer來操做堆外內存。如上圖,雖然堆外內存不受JVM管控,可是堆內存中會持有對它的引用,以便進行GC。

 

提一個問題:整體來看,JVM把內存劃分爲「棧(stack)」與「堆(heap)」兩大類,爲什麼要這樣設計?

 

我的理解,程序運行時,內存中的信息大體分爲兩類,一是跟程序執行邏輯相關的指令數據,這類數據一般不大,並且生命週期短;一是跟對象實例相關的數據,這類數據可能會很大,並且能夠被多個線程長時間內反覆共用,好比字符串常量、緩存對象這類。

 

將這兩類特色不一樣的數據分開管理,體現了軟件設計上「模塊隔離」的思想。比如咱們一般會把後端service與前端website解耦相似,也更便於內存管理。

 

2、GC垃圾回收原理

2.1 如何判斷對象是垃圾 ? 

有兩種經典的判斷方法,借用網友的圖(文中最後有給出連接):

 

引用計數法,思路很簡單,可是若是出現循環引用,即:A引用B,B又引用A,這種狀況下就很差辦了,因此JVM中使用了另外一種稱爲「可達性分析」的判斷方法:

 

仍是剛纔的循環引用問題(也是某些公司面試官可能會問到的問題),若是A引用B,B又引用A,這2個對象是否能被GC回收? 

 

答案:關鍵不是在於A、B之間是否有引用,而是A、B是否能夠一直向上追溯到GC Roots。若是與GC Roots沒有關聯,則會被回收,不然將繼續存活。

 

上圖是一個用「可達性分析」標記垃圾對象的示例圖,灰色的對象表示不可達對象,將等待回收。

 

2.2 哪些內存區域須要GC ?

 

在第一部分JVM內存佈局中,咱們知道了thread獨享的區域:PC Regiester、JVM Stack、Native Method Stack,其生命週期都與線程相同(即:與線程共生死),因此無需GC。線程共享的Heap區、Method Area則是GC關注的重點對象。

 

2.3 經常使用的GC算法

1)mark-sweep 標記清除法

 

如上圖,黑色區域表示待清理的垃圾對象,標記出來後直接清空。該方法簡單快速,可是缺點也很明顯,會產生不少內存碎片。

 

2)mark-copy 標記複製法

 

 

思路也很簡單,將內存對半分,老是保留一塊空着(上圖中的右側),將左側存活的對象(淺灰色區域)複製到右側,而後左側所有清空。避免了內存碎片問題,可是內存浪費很嚴重,至關於只能使用50%的內存。

 

3)mark-compact 標記-整理(也稱標記-壓縮)法

 

避免了上述兩種算法的缺點,將垃圾對象清理掉後,同時將剩下的存活對象進行整理挪動(相似於windows的磁盤碎片整理),保證它們佔用的空間連續,這樣就避免了內存碎片問題,可是整理過程也會下降GC的效率。

 

4)generation-collect 分代收集算法

上述三種算法,每種都有各自的優缺點,都不完美。在現代JVM中,每每是綜合使用的,通過大量實際分析,發現內存中的對象,大體能夠分爲兩類:有些生命週期很短,好比一些局部變量/臨時對象,而另外一些則會存活好久,典型的好比websocket長鏈接中的connection對象,以下圖:

縱向y軸能夠理解分配內存的字節數,橫向x軸理解爲隨着時間流逝(伴隨着GC),能夠發現大部分對象其實至關短命,不多有對象能在GC後活下來。所以誕生了分代的思想,以Hotspot爲例(JDK 7):

 

將內存分紅了三大塊:年青代(Young Genaration),老年代(Old Generation),永久代(Permanent Generation),其中Young Genaration更是又細爲分eden,S0,S1三個區。

 

結合咱們常用的一些jvm調優參數後,一些參數能影響的各區域內存大小值,示意圖以下:

注:jdk8開始,用MetaSpace區取代了Perm區(永久代),因此相應的jvm參數變成-XX:MetaspaceSize 及 -XX:MaxMetaspaceSize。

 

以Hotspot爲例,咱們來分析下GC的主要過程:

剛開始時,對象分配在eden區,s0(即:from)及s1(即:to)區,幾乎是空着。

 

隨着應用的運行,愈來愈多的對象被分配到eden區。

 

 

當eden區放不下時,就會發生minor GC(也被稱爲young GC),第1步固然是要先標識出不可達垃圾對象(即:下圖中的黃色塊),而後將可達對象,移動到s0區(即:4個淡藍色的方塊挪到s0區),而後將黃色的垃圾塊清理掉,這一輪事後,eden區就成空的了。

 

注:這裏其實已經綜合運用了「【標記-清理eden】 + 【標記-複製 eden->s0】」算法。

 

 

隨着時間推移,eden若是又滿了,再次觸發minor GC,一樣仍是先作標記,這時eden和s0區可能都有垃圾對象了(下圖中的黃色塊),注意:這時s1(即:to)區是空的,s0區和eden區的存活對象,將直接搬到s1區。而後將eden和s0區的垃圾清理掉,這一輪minor GC後,eden和s0區就變成了空的了。

 

 

繼續,隨着對象的不斷分配,eden空可能又滿了,這時會重複剛纔的minor GC過程,不過要注意的是,這時候s0是空的,因此s0與s1的角色其實會互換,即:存活的對象,會從eden和s1區,向s0區移動。而後再把eden和s1區中的垃圾清除,這一輪完成後,eden與s1區變成空的,以下圖。

 

 

對於那些比較「長壽」的對象一直在s0與s1中挪來挪去,一來很佔地方,並且也會形成必定開銷,下降gc效率,因而有了「代齡(age)」及「晉升」。

 

對象在年青代的3個區(edge,s0,s1)之間,每次從1個區移到另1區,年齡+1,在young區達到必定的年齡閾值後,將晉升到老年代。下圖中是8,即:挪動8次後,若是還活着,下次minor GC時,將移動到Tenured區。

 

 

下圖是晉升的主要過程:對象先分配在年青代,通過屢次Young GC後,若是對象還活着,晉升到老年代。

 

 

若是老年代,最終也放滿了,就會發生major GC(即Full GC),因爲老年代的的對象一般會比較多,由於標記-清理-整理(壓縮)的耗時一般會比較長,會讓應用出現卡頓的現象,這也是爲何不少應用要優化,儘可能避免或減小Full GC的緣由。

 

 

注:上面的過程主要來自oracle官網的資料,可是有一個細節官網沒有提到,若是分配的新對象比較大,eden區放不下,可是old區能夠放下時,會直接分配到old區(即沒有晉升這一過程,直接到老年代了)。

 

下圖引自阿里出品的《碼出高效-Java開發手冊》一書,梳理了GC的主要過程。

 

  

3、垃圾回收器

不算出現的神器ZGC,歷史上出現過7種經典的垃圾回收器。

 

這些回收器都是基於分代的,把G1除外,按回收的分代劃分,橫線以上的3種:Serial ,ParNew, Parellel Scavenge都是回收年青代的,橫線如下的3種:CMS,Serial Old, Parallel Old 都是回收老年代的。

 

3.1 Serial 收集器

單線程用標記-複製算法,快刀斬亂麻,單線程的好處避免上下文切換,早期的機器,大可能是單核,也比較實用。但執行期間,會發生STW(Stop The World)。

 

3.2 ParNew 收集器

Serial的多線程版本,一樣會STW,在多核機器上會更適用。

 

3.3 Parallel Scavenge 收集器

ParNew的升級版本,主要區別在於提供了兩個參數:-XX:MaxGCPauseMillis 較大垃圾回收停頓時間;-XX:GCTimeRatio 垃圾回收時間與總時間佔比,經過這2個參數,能夠適當控制回收的節奏,更關注於吞吐率,即總時間與垃圾回收時間的比例。

 

3.4 Serial Old 收集器

由於老年代的對象一般比較多,佔用的空間一般也會更大,若是採用複製算法,得留50%的空間用於複製,至關不划算,並且由於對象多,從1個區,複製到另1個區,耗時也會比較長,因此老年代的收集,一般會採用「標記-整理」法。從名字就能夠看出來,這是單線程(串行)的, 依然會有STW。

 

3.5 Parallel Old 收集器

一句話:Serial Old的多線程版本。

 

3.6 CMS 收集器

全稱:Concurrent Mark Sweep,從名字上看,就能猜出它是併發多線程的。這是JDK 7中普遍使用的收集器,有必要多說一下,借一張網友的圖說話:

 

相對3.4 Serial Old收集器或3.5 Parallel Old收集器而言,這個明顯要複雜多了,分爲4個階段:

 

1)Inital Mark 初始標記:主要是標記GC Root開始的下級(注:僅下一級)對象,這個過程會STW,可是跟GC Root直接關聯的下級對象不會不少,所以這個過程其實很快。

 

2)Concurrent Mark 併發標記:根據上一步的結果,繼續向下標識全部關聯的對象,直到這條鏈上的最盡頭。這個過程是多線程的,雖然耗時理論上會比較長,可是其它工做線程並不會阻塞,沒有STW。

 

3)Remark 再標誌:爲啥還要再標記一次?由於第2步並無阻塞其它工做線程,其它線程在標識過程當中,頗有可能會產生新的垃圾。

 

試想下,高鐵上的垃圾清理員,從車箱一頭開始吆喝「有須要扔垃圾的乘客,請把垃圾扔一下」,一邊工做一邊向前走,等走到車箱另外一頭時,剛纔走過的位置上,可能又有乘客產生了新的空瓶垃圾。因此,要徹底把這個車箱清理乾淨的話,她應該喊一下:全部乘客不要再扔垃圾了(STW),而後把新產生的垃圾收走。固然,由於剛纔已經把收過一遍垃圾,因此此次收集新產生的垃圾,用不了多長時間(即:STW時間不會很長)。

 

4)Concurrent Sweep:並行清理,這裏使用多線程以「Mark Sweep-標記清理」算法,把垃圾清掉,其它工做線程仍然能繼續支行,不會形成卡頓。

 

等等,剛纔咱們不是提到過「標記清理」法,會留下不少內存碎片嗎?確實,可是也沒辦法,若是換成「Mark Compact標記-整理」法,把垃圾清理後,剩下的對象也順便排整理,會致使這些對象的內存地址發生變化,別忘了,此時其它線程還在工做,若是引用的對象地址變了,就天下大亂了。

 

另外,因爲這一步是並行處理,並不阻塞其它線程,因此還有一個副使用,在清理的過程當中,仍然可能會有新垃圾對象產生,只能等到下一輪GC,纔會被清理掉。

 

雖然仍不完美,可是從這4步的處理過程來看,以往收集器中最讓人詬病的長時間STW,經過上述設計,被分解成二次短暫的STW,因此從整體效果上看,應用在GC期間卡頓的狀況會大大改善,這也是CMS一度十分流行的重要緣由。

 

3.7 G1 收集器

G1的全稱是Garbage-First,爲何叫這個名字,呆會兒會詳細說明。鑑於CMS的一些不足以外,好比: 老年代內存碎片化,STW時間雖然已經改善了不少,可是仍然有提高空間。G1就橫空出世了,它對於heap區的內存劃思路很新穎,有點算法中分治法「分而治之」的味道。

 

以下圖,G1將heap內存區,劃分爲一個個大小相等(1-32M,2的n次方)、內存連續的Region區域,每一個region都對應Eden、Survivor 、Old、Humongous四種角色之一,可是region與region之間不要求連續。

 

注:Humongous,簡稱H區是專用於存放超大對象的區域,一般>= 1/2 Region Size,且只有Full GC階段,纔會回收H區,避免了頻繁掃描、複製/移動大對象。

 

全部的垃圾回收,都是基於1個個region的。JVM內部知道,哪些region的對象最少(即:該區域最空),老是會優先收集這些region(由於對象少,內存相對較空,確定快),這也是Garbage-First得名的由來,G便是Garbage的縮寫, 1即First。

 

 

G1 Young GC

 

young GC前:

 

young GC後:

 

 

理論上講,只要有一個Empty Region(空區域),就能夠進行垃圾回收。

 

 

因爲region與region之間並不要求連續,而使用G1的場景一般是大內存,好比64G甚至更大,爲了提升掃描根對象和標記的效率,G1使用了二個新的輔助存儲結構:

 

Remembered Sets:簡稱RSets,用於根據每一個region裏的對象,是從哪指向過來的(即:誰引用了我),每一個Region都有獨立的RSets。(Other Region -> Self Region)。

 

Collection Sets :簡稱CSets,記錄了等待回收的Region集合,GC時這些Region中的對象會被回收(copied or moved)。

 

RSets的引入,在YGC時,將年青代Region的RSets作爲根對象,能夠避免掃描老年代的region,能大大減輕GC的負擔。注:在老年代收集Mixed GC時,RSets記錄了Old->Old的引用,也能夠避免掃描全部Old區。

 

Old Generation Collection(也稱爲 Mixed GC)

按oracle官網文檔描述分爲5個階段:Initial Mark(STW) -> Root Region Scan -> Cocurrent Marking -> Remark(STW) -> Copying/Cleanup(STW && Concurrent)

 

注:也有不少文章會把Root Region Scan省略掉,合併到Initial Mark裏,變成4個階段。

 

存活對象的「初始標記」依賴於Young GC,GC 日誌中會記錄成young字樣。

 

2019-06-09T15:24:37.086+0800: 500993.392: [GC pause (G1 Evacuation Pause) (young), 0.0493588 secs]

   [Parallel Time: 41.9 ms, GC Workers: 8]

      [GC Worker Start (ms): Min: 500993393.7, Avg: 500993393.7, Max: 500993393.7, Diff: 0.1]

      [Ext Root Scanning (ms): Min: 1.5, Avg: 2.2, Max: 4.4, Diff: 2.8, Sum: 17.2]

      [Update RS (ms): Min: 15.8, Avg: 18.1, Max: 18.9, Diff: 3.1, Sum: 144.8]

         [Processed Buffers: Min: 110, Avg: 144.9, Max: 163, Diff: 53, Sum: 1159]

      [Scan RS (ms): Min: 4.7, Avg: 5.0, Max: 5.1, Diff: 0.4, Sum: 39.7]

      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]

      [Object Copy (ms): Min: 16.4, Avg: 16.5, Max: 16.6, Diff: 0.2, Sum: 132.0]

      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]

         [Termination Attempts: Min: 1, Avg: 4.9, Max: 7, Diff: 6, Sum: 39]

      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.1, Diff: 0.1, Sum: 0.3]

      [GC Worker Total (ms): Min: 41.7, Avg: 41.8, Max: 41.8, Diff: 0.1, Sum: 334.1]

      [GC Worker End (ms): Min: 500993435.5, Avg: 500993435.5, Max: 500993435.5, Diff: 0.0]

   [Code Root Fixup: 0.0 ms]

   [Code Root Purge: 0.0 ms]

   [Clear CT: 0.2 ms]

   [Other: 7.2 ms]

      [Choose CSet: 0.0 ms]

      [Ref Proc: 4.3 ms]

      [Ref Enq: 0.1 ms]

      [Redirty Cards: 0.1 ms]

      [Humongous Register: 0.1 ms]

      [Humongous Reclaim: 0.1 ms]

      [Free CSet: 0.6 ms]

   [Eden: 1340.0M(1340.0M)->0.0B(548.0M) Survivors: 40.0M->64.0M Heap: 2868.2M(12.0G)->1499.8M(12.0G)]

 [Times: user=0.35 sys=0.00, real=0.05 secs]

 

 

併發標記過程當中,若是發現某些region全是空的,會被直接清除。

 

 

進入從新標記階段。

 

 

併發複製/清查階段。這個階段,Young區和Old區的對象有可能會被同時清理。GC日誌中,會記錄爲mixed字段,這也是G1的老年代收集,也稱爲Mixed GC的緣由。 

 

2019-06-09T15:24:23.959+0800: 500980.265: [GC pause (G1 Evacuation Pause) (mixed), 0.0885388 secs]

   [Parallel Time: 74.2 ms, GC Workers: 8]

      [GC Worker Start (ms): Min: 500980270.6, Avg: 500980270.6, Max: 500980270.6, Diff: 0.1]

      [Ext Root Scanning (ms): Min: 1.7, Avg: 2.2, Max: 4.1, Diff: 2.4, Sum: 17.3]

      [Update RS (ms): Min: 11.7, Avg: 13.7, Max: 14.3, Diff: 2.6, Sum: 109.8]

         [Processed Buffers: Min: 136, Avg: 141.5, Max: 152, Diff: 16, Sum: 1132]

      [Scan RS (ms): Min: 42.5, Avg: 42.9, Max: 43.1, Diff: 0.5, Sum: 343.1]

      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]

      [Object Copy (ms): Min: 14.9, Avg: 15.2, Max: 15.4, Diff: 0.5, Sum: 121.7]

      [Termination (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]

         [Termination Attempts: Min: 1, Avg: 8.2, Max: 11, Diff: 10, Sum: 66]

      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.2]

      [GC Worker Total (ms): Min: 74.0, Avg: 74.0, Max: 74.1, Diff: 0.1, Sum: 592.3]

      [GC Worker End (ms): Min: 500980344.6, Avg: 500980344.6, Max: 500980344.6, Diff: 0.0]

   [Code Root Fixup: 0.0 ms]

   [Code Root Purge: 0.0 ms]

   [Clear CT: 0.5 ms]

   [Other: 13.9 ms]

      [Choose CSet: 4.1 ms]

      [Ref Proc: 1.8 ms]

      [Ref Enq: 0.1 ms]

      [Redirty Cards: 0.2 ms]

      [Humongous Register: 0.1 ms]

      [Humongous Reclaim: 0.1 ms]

      [Free CSet: 5.6 ms]

   [Eden: 584.0M(584.0M)->0.0B(576.0M) Survivors: 28.0M->36.0M Heap: 4749.3M(12.0G)->2930.0M(12.0G)]

 [Times: user=0.61 sys=0.00, real=0.09 secs]

 

上圖是,老年代收集完後的示意圖。

 

經過這幾個階段的分析,雖然看上去不少階段仍然會發生STW,可是G1提供了一個預測模型,經過統計方法,根據歷史數據來預測本次收集,須要選擇多少個Region來回收,儘可能知足用戶的預期停頓值(-XX:MaxGCPauseMillis參數可指定預期停頓值)。

 

注:若是Mixed GC仍然效果不理想,跟不上新對象分配內存的需求,會使用Serial Old GC(Full GC)強制收集整個Heap。

 

小結:與CMS相比,G1有內存整理過程(標記-壓縮),避免了內存碎片;STW時間可控(能預測GC停頓時間)。

 

3.8 ZGC (截止目前史上較好的GC收集器)

在G1的基礎上,作了不少改進(JDK 11開始引入)

 

3.8.1 動態調整大小的Region

G1中每一個Region的大小是固定的,建立和銷燬Region,能夠動態調整大小,內存使用更高效。

 

3.8.2 不分代,幹掉了RSets

G1中每一個Region須要藉助額外的RSets來記錄「誰引用了我」,佔用了額外的內存空間,每次對象移動時,RSets也須要更新,會產生開銷。

 

注:ZGC沒有爲止,沒有實現分代機制,每次都是併發的對全部region進行回收,不象G1是增量回收,因此用不着RSets。不分代的帶來的可能性能降低,會用下面立刻提到的Colored Pointer && Load Barrier來優化。

 

3.8.3 帶顏色的指針 Colored Pointer

 

這裏的指針相似java中的引用,意爲對某塊虛擬內存的引用。ZGC採用了64位指針(注:目前只支持Linux 64位系統),將42-45這4個bit位置賦予了不一樣的含義,即所謂的顏色標誌位,也換爲指針的metadata。

 

finalizable位:僅finalizer(類比c++中的析構函數)可訪問;

remap位:指向對象當前()的內存地址,參考下面提到的relocation;

marked0 && marked1 位:用於標誌可達對象;

 

這4個標誌位,同一時刻只會有1個位置是1。每當指針對應的內存數據發生變化,好比內存被移動,顏色會發生變化。

 

3.8.4 讀屏障 Load Barrier

傳統GC作標記時,爲了防止其它線程在標記期間修改對象,一般會簡單的STW。而ZGC有了Colored Pointer後,引入了所謂的讀屏障,當指針引用的內存正被移動時,指針上的顏色就會變化,ZGC會先把指針更新成狀態,而後再返回。(你們能夠回想下java中的volatile關鍵字,有殊途同歸之妙),這樣僅讀取該指針時可能會略有開銷,而不用將整個heap STW。

 

3.8.5 重定位 relocation

 

如上圖,在標記過程當中,先從Roots對象找到了直接關聯的下級對象1,2,4。

 

而後繼續向下層標記,找到了5,8對象, 此時已經能夠斷定 3,6,7爲垃圾對象。

 

若是按常規思路,通常會將8從最右側的Region移動或複製到中間的Region,而後再將中間Region的3幹掉,最後再對中間Region作壓縮compact整理。但ZGC作得更高明,它直接將4,5複製到了一個空的新Region就完事了,而後中間的2個Region直接廢棄,或理解爲「釋放」,作爲下次回收的「新」Region。這樣的好處是避免了中間Region的compact整理過程。

 

最後,指針從新調整爲正確的指向(即:remap),並且上一階段的remap與下一階段的mark是混在一塊兒處理的,相對更高效。

 

Remap的流程圖以下:

 

3.8.6 多重映射 Multi-Mapping

這個優化,說實話沒徹底看懂,只能談下本身的理解(若是有誤,歡迎指正)。虛擬內存與實際物理內存,OS會維護一個映射關係,才能正常使用。以下圖:

 

zgc的64位顏色指針,在解除映射關係時,代價較高(須要屏蔽額外的42-45的顏色標誌位)。考慮到這4個標誌位,同1時刻,只會有1位置成1(以下圖),另外finalizable標誌位,永遠不但願被解除映射綁定(可不用考慮映射問題)。

 

因此剩下3種顏色的虛擬內存,能夠都映射到同1段物理內存。即映射覆用,或者更通俗點講,原本3種不一樣顏色的指針,哪怕0-41位徹底相同,也須要映射到3段不一樣的物理內存,如今只須要映射到同1段物理內存便可。

 

3.8.7 支持NUMA架構

NUMA是一種多核服務器的架構,簡單來說,一個多核服務器(好比2core),每一個cpu都有屬於本身的存儲器,會比訪問另外一個核的存儲器會慢不少(相似於就近訪問更快)。

 

相對以前的GC算法,ZGC首次支持了NUMA架構,申請堆內存時,判斷當前線程屬是哪一個CPU在執行,而後就近申請該CPU能使用的內存。

 

小結:革命性的ZGC通過上述一堆優化後,每次GC整體卡頓時間按官方說法<10ms。注:啓用zgc,須要設置-XX:+UnlockExperimentalVMOptions -XX:+UseZGC。

 

4、實戰練習

前面介紹了一堆理論,最後來作一個小的練習,下面是一段模擬OOM的測試代碼,咱們在G一、CMS這二種經常使用垃圾回收器上試驗一下。

 

import sun.misc.Unsafe;

 

import java.lang.reflect.Field;

import java.util.ArrayList;

import java.util.List;

 

public class OOMTest {

 

    public static void main(String[] args) {

        OOMTest test = new OOMTest();

        //heap區OOM測試     

        //test.heapOOM();

 

        //虛擬機棧和本地方法棧溢出

        //test.stackOverflow();

 

        //metaspace OOM測試

        //test.metaspaceOOM();

 

        //堆外內存 OOM測試

        //test.directOOM();

    }

 

    /**

     * heap OOM測試

     */

    public void heapOOM() {

        List<OOMTest> list = new ArrayList<>();

        while (true) {

            list.add(new OOMTest());

        }

    }

 

    private int stackLength = 1;

 

    public void stackLeak() {

        stackLength += 1;

        stackLeak();

    }

 

    /**

     * VM Stack / Native method Stack 溢出測試

     */

    public void stackOverflow() {

        OOMTest test = new OOMTest();

        try {

            test.stackLeak();

        } catch (Throwable e) {

            System.out.println("stack length:" + test.stackLength);

            throw e;

        }

    }

 

    public void genString() {

        List<String> list = new ArrayList<>();

        int i = 0;

        while (true) {

            list.add("string-" + i);

            i++;

        }

    }

 

    /**

     * metaspace/常量池 OOM測試

     */

    public void metaspaceOOM() {

        OOMTest test = new OOMTest();

        test.metaspaceOOM();

    }

 

    public void allocDirectMemory() {

        final int _1MB = 1024 * 1024;

 

        Field unsafeField = Unsafe.class.getDeclaredFields()[0];

        unsafeField.setAccessible(true);

        Unsafe unsafe = null;

        try {

            unsafe = (Unsafe) unsafeField.get(null);

        } catch (IllegalArgumentException | IllegalAccessException e) {

            e.printStackTrace();

        }

 

        while (true) {

            unsafe.allocateMemory(_1MB);

        }

    }

 

    /**

     * 堆外內存OOM測試

     */

    public void directOOM() {

        OOMTest test = new OOMTest();

        test.allocDirectMemory();

    }

}

 

4.1 openjdk 11.0.3 環境:+ G1回收

4.1.1 驗證heap OOM 

把main方法中的test.heapOOM()行,註釋打開,而後命令行下運行:

java -Xmx10M -XX:+UseG1GC -Xlog:gc* -Xlog:gc:gc.log -XX:+HeapDumpBeforeFullGC  OOMTest.java

 

最後會輸出:

[1.892s][info][gc             ] GC(42) Concurrent Cycle 228.393ms

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

        at java.base/java.util.Arrays.copyOf(Arrays.java:3689)

        at java.base/java.util.ArrayList.grow(ArrayList.java:237)

        at java.base/java.util.ArrayList.grow(ArrayList.java:242)

        at java.base/java.util.ArrayList.add(ArrayList.java:485)

        at java.base/java.util.ArrayList.add(ArrayList.java:498)

        at oom.OOMTest.heapOOM(OOMTest.java:37)

        at oom.OOMTest.main(OOMTest.java:16)

[1.895s][info][gc,heap,exit   ] Heap

 

其中 OutOfMemoryError:Java heap space即表示heap OOM。

 

4.1.2 驗證stack溢出

把main方法中的test.stackOverflow()行,註釋打開,而後命令行下運行:

java -Xmx20M -Xss180k -XX:+UseG1GC -Xlog:gc* -Xlog:gc:gc.log  -XX:+HeapDumpBeforeFullGC OOMTest.java

 

最後會輸出:

[0.821s][info][gc           ] GC(4) Pause Young (Normal) (G1 Evacuation Pause) 12M->7M(20M) 5.245ms

[0.821s][info][gc,cpu       ] GC(4) User=0.00s Sys=0.00s Real=0.00s

stack length:1699

Exception in thread "main" java.lang.StackOverflowError

        at oom.OOMTest.stackLeak(OOMTest.java:45)

        at oom.OOMTest.stackLeak(OOMTest.java:45)

 

其中 StackOverflowError 即表示stack棧區內存不足,致使溢出。

 

4.1.3 驗證metaspace OOM

把main方法中的test.metaspaceOOM()行,註釋打開,而後命令行下運行:

 

java -Xmx20M -XX:MaxMetaspaceSize=10M -XX:+UseG1GC -Xlog:gc*  -Xlog:gc:gc.log -XX:+HeapDumpBeforeFullGC OOMTest.java

 

最後會輸出:

[0.582s][info][gc,metaspace,freelist,oom]

Exception in thread "main" java.lang.OutOfMemoryError: Metaspace

[0.584s][info][gc,heap,exit             ] Heap

 

其中 OutOfMemoryError: Metaspace 即表示Metaspace區OOM。

 

4.1.4 驗證堆外內存OOM

把main方法中的test.directOOM()行,註釋打開,而後命令行下運行:

 

最後會輸出:

[0.842s][info][gc,cpu       ] GC(4) User=0.06s Sys=0.00s Real=0.01s

Exception in thread "main" java.lang.OutOfMemoryError

        at java.base/jdk.internal.misc.Unsafe.allocateMemory(Unsafe.java:616)

...

 

其中OutOfMemoryError行並無輸出具體哪一個區(注:堆外內存不屬於JVM內存中的任何一個區,因此沒法輸出),但緊接着有一行jdk.internal.misc.Unsafe.allocateMemory 能夠看出是「堆外內存直接分配」致使的異常。

 

4.2 openjdk 1.8.0_212 + CMS回收

jdk1.8下,java命令沒法直接運行.java文件,必須先編譯,即:

 

javac OOMTest.java -encoding utf-8

 

(注:-encoding utf-8 是爲了防止中文註釋javac沒法識別)成功後,會生成OOMTest.class文件, 而後再能夠參考下面的命令進行測試。

 

4.2.1 heap OOM測試:

java -Xmx10M -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log -XX:+HeapDumpOnOutOfMemoryError OOMTest

 

4.2.2 驗證stack溢出

java -Xmx10M -Xss128k -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log -XX:+HeapDumpOnOutOfMemoryError OOMTest

 

4.2.3 驗證metaspace OOM

java -Xmx20M -XX:+UseConcMarkSweepGC -XX:MaxMetaspaceSize=10M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log -XX:+HeapDumpOnOutOfMemoryError OOMTest

 

4.2.4 驗證堆外內存OOM

java -Xmx20M -XX:+UseConcMarkSweepGC -XX:MaxDirectMemorySize=10M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log -XX:+HeapDumpOnOutOfMemoryError OOMTest

 

4.3 GC日誌查看工具

生成的gc日誌文件,能夠用開源工具GCViewer查看,這是一個純java寫的GUI程序,使用很簡單,File→Open File 選擇gc日誌文件便可。目前支持CMS/G1生成的日誌文件,另外若是GC文件過大時,可能打不開。

GCViewer能夠很方便的統計出GC的類型,次數,停頓時間,年青代/老年代的大小等,還有圖表顯示,很是方便。 

 

參考文章:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5

https://blog.csdn.net/heart_mine/article/details/79495032

https://www.programcreek.com/2013/04/jvm-run-time-data-areas/

https://javapapers.com/core-java/java-jvm-run-time-data-areas/

https://javapapers.com/core-java/java-jvm-memory-types/

https://cloud.tencent.com/developer/article/1152616

https://www.jianshu.com/p/17e72bb01bf1

http://calvin1978.blogcn.com/articles/directbytebuffer.html

https://www.cnkirito.moe/nio-buffer-recycle/

https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

http://inbravo.github.io/html/jvm.html

https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html

https://segmentfault.com/a/1190000009783873

https://segmentfault.com/a/1190000016551339

https://www.team-bob.org/things-about-java-garbage-collection-1/2/

https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html

https://tech.meituan.com/2016/09/23/g1.html

https://mp.weixin.qq.com/s/KUCs_BJUNfMMCO1T3_WAjw

https://www.baeldung.com/jvm-zgc-garbage-collector

http://xxfox.perfma.com/jvm/

https://wiki.openjdk.java.net/display/zgc/Main

http://cr.openjdk.java.net/~pliden/slides/ZGC-FOSDEM-2018.pdf

http://www.ishenping.com/ArtInfo/43701.html

http://likehui.top/2019/04/11/ZGC-%E7%89%B9%E6%80%A7%E8%A7%A3%E8%AF%BB/

【原文連接】http://java.dataguru.cn/article-15078-1.html

相關文章
相關標籤/搜索