做者簡介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/