一文看懂 JVM 內存布局及 GC 原理


注:本文篇幅較長,且需要有一定的java基礎,建議各位看官,備好瓜子、飲料、小板凳,擺個讓自己舒服的姿勢,慢慢細看^_^, 文中素材均來自互聯網(末尾有給出參考文章鏈接)。

一、JVM運行時內存布局

java 8虛擬機規范的原始表達:(jvm)Run-Time Data Areas, 暫時翻譯為"jvm運行時內存布局"。

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

 這6塊區域按是否被線程共享,可以分為二大類:

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

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

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

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

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

上述3類區域,生命周期與Thread相同,即:線程創建時,相應的內存區創建,線程銷毀時,釋放相應內存。

 

另一類是所有線程共享的:

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

注:Heap被所有線程共享,如果嚴格意義上摳字眼的話,也不完正確,事實上,由於TLAB的存在,為了防止並發對象分配時,多個對象分配到同1塊內存,heap中的TLAB區域,在分配時,是被線程獨占寫入的。

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解耦類似),也更便於內存管理。

 

二、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算法:

a. mark-sweep 標記清除法

點擊查看原圖

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

 

b. mark-copy 標記復制法

點擊查看原圖

思路也很簡單,將內存對半分,總是保留一塊空着(上圖中的右側),將左側存活的對象(淺灰色區域)復制到右側,然后左側全部清空。避免了內存碎片問題,但是內存浪費很嚴重,相當於只能使用50%的內存。

 

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

點擊查看原圖

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

 

d. 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的主要過程。

 

 

 

三、垃圾回收器

不算最新出現的神器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四種角色之一(注:Humongous,簡稱H區是專用於存放超大對象的區域,通常>= 1/2 Region Size,且只有Full GC階段,才會回收H區,避免了頻繁掃描、復制/移動大對象),但是region與region之間不要求連續。所有的垃圾回收,都是基於1個個region的。JVM內部知道,哪些region的對象最少(即:該區域最空),總是會優先收集這些region(因為對象少,內存相對較空,肯定快),這也是Garbage-First得名的由來,G即是Garbage的縮寫, 1即First(第1)。

 點擊查看原圖

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的大小是固定的,但ZGC創建和銷毀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)

 

最后附上一段OOM的常用測試代碼:

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();
    }
}

  

命令行測試方法:

一、 openjdk 11.0.3 環境: + G1回收

a、驗證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

b、驗證stack溢出

把main方法中的test.stackOverflow()行,注釋打開,然后命令行下運行:

java -Xmx20M -Xss180k -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

d、驗證堆外內存OOM

把main方法中的test.directOOM()行,注釋打開,然后命令行下運行:

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

最后會輸出:

[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 可以看出是"堆外內存直接分配"導致的異常

 

二、openjdk 1.8.0_212 + CMS回收

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

java OOMTest.java 成功后,會生成OOMTest.class文件, 然后再可以參考下面的命令進行測試:

a、heap OOM測試

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

b、驗證stack溢出

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

c、驗證metaspace OOM

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

d、驗證堆外內存OOM

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

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

點擊查看原圖

點擊查看原圖

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

 

注:本文已經被InfoQ收錄 、攜程技術公眾號收錄,歡迎轉載,但請注明出處,謝謝。

 

參考文章:

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/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM