原文出自【匠心零度】(公眾號:匠心零度)轉載請註明原創出處,謝謝!
題目回顧
JVM菜鳥進階高手之路十三,問題現象就是相同的程式碼,jvm引數不一樣,表現的現象不一樣。
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
byte[] all1 = new byte[2 * _1MB];
byte[] all2 = new byte[2 * _1MB];
byte[] all3 = new byte[2 * _1MB];
byte[] all4 = new byte[7 * _1MB];
System.in.read();
}
jvm引數配置如下:
-Xmx20m
-Xms20m
-Xmn10m
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=75
透過jstat命令,檢視結果如下:
關於jstat命令詳情可以參考:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html
jvm引數調整如下:
-Xmx20m
-Xms20m
-Xmn10m
-XX:+UseParNewGC
透過jstat命令,檢視結果如下:
說明
上面的題目僅僅是一個切入點而已,希望透過一個切入點把jvm的一些基礎知識剛剛好說明下,順便解答下上面的現象。
記憶體相關簡單說明
圖中引數:-Xms設定最小堆空間大小(一般建議和-Xmx一樣)。 -Xmx設定最大堆空間大小。 -Xmn設定新生代大小。 -XX:MetaspaceSize設定最小元資料空間大小。 -XX:MaxMetaspaceSize設定最大元資料空間大小。 -Xss設定每個執行緒的堆疊大小(這裡有個故事,3年前用正則運算式,後續有空正則運算式再說)。
備註:tenured空間就用減法操作即可明白,堆空間大小減去年輕代大小就可以了。
說到這裡,下麵這個幾個引數應該明白了。
-Xmx20m
-Xms20m
-Xmn10m
備註:引數-XX:SurvivorRatio用來表示s0、s1、eden之間的比例,預設情況下-XX:SurvivorRatio=8表示 s0:s1:eden=1:1:8。
得出結論:eden=8M,s0=1M,s1=1M,tenured=10M。
JVM垃圾回收期組合
還有一個問題需要解決,jvm垃圾回收器方面,下麵這個圖,我是我的JVM菜鳥進階高手之路八(一些細節),裡面的,當時依稀記得這個圖應該是飛哥發給我的。
由於那個時候jdk9還沒有出來,可以去看看我的JVM菜鳥進階高手之路十二(jdk9、JVM方面變化, 蹭熱度),雖然有些有些稍微去掉了,但是整體的組合還是影響不大。
由於上面的2個jvm引數都是基於分代收集演演算法的(先不考慮G1)
-
依據物件的存活週期進行分為新生代,老年代。
-
根據不同代的特點,選取合適的收集演演算法
-
新生代,適合複製演演算法
-
老年代,適合標記清理或者標記壓縮
複製演演算法
-
將原有的記憶體空間分為兩塊,每次只使用其中一塊,在垃圾回收時,將正在使用的記憶體中的存活物件複製到未使用的記憶體塊中,之後,清除正在使用的記憶體塊中的所有物件,交換兩個記憶體的角色,完成垃圾回收。
-
不適用於存活物件較多的場合 如老年代。(年輕代物件基本都是朝生夕滅所以特別適合,由於那樣的話複製就少,如果類似老年代有大量存活物件,那麼進行複製演演算法效能就不是特別好了)
備註:使用複製演演算法的優點:每次都是對其中的一塊進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況了,使用複製演演算法的缺點:對空間有一定浪費,所以複製空間一般不會特別大。
標記清除標記-清除演演算法將垃圾回收分為兩個階段:標記階段和清除階段。在標記階段,首先先找出根物件,標記所有從根節點開始的可達物件。因此,未被標記的物件就是未被取用的垃圾物件。然後,在清除階段,清除所有未被標記的物件。
備註:java根物件:
虛擬機器棧中取用的物件。
方法區中類靜態屬性物體取用的物件。
方法區中常量取用的物件。
本地方法棧中JNI取用的物件。
等等。
標記清除演演算法缺點:標記清除會產生不連續的記憶體碎片,如果空間記憶體碎片過多會導致,當程式在執行過程中需要分配空間時找不到足夠的連續空間而不得不提前觸發一次垃圾收集動作(根據演演算法不一樣效果也不一樣)。
標記壓縮標記-壓縮演演算法適合用於存活物件較多的場合,如老年代。它在標記-清除演演算法的基礎上做了一些最佳化。和標記-清除演演算法一樣,標記-壓縮演演算法也首先需要從根節點開始,對所有可達物件做一次標記。但之後,它並不簡單的清理未標記的物件,而是將所有的存活物件壓縮到記憶體的一端。之後,清理邊界外所有的空間。
備註:這樣帶來的好處就是不會引數記憶體碎片問題了。
上面已經說明瞭這麼多了,我們可以繼續說明上題中JVM的其他引數了。
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC 表示新生代使用ParNew並行收集器,-XX:+UseConcMarkSweepGC 表示老年代使用CMS回收器(CMS收集器是基於“標記-清除”演演算法實現的,特別提醒由於CMS是標記清除演演算法實現的所以是存在碎片問題的)。
可以去看看我的JVM菜鳥進階高手之路六(JVM每隔一小時執行一次Full GC)、以及JVM菜鳥進階高手之路七(tomcat調優以及tomcat7、8效能對比)圖片就取的這兩篇裡面的。
備註:透過jstat -gcutil pid 檢視的FGC這列的時候,CMS gc通常都是+2一次的,由於CMS-initial-mark和CMS-remark會stop-the-world。
所以看到這個圖的FGC應該沒有什麼問題了吧。
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=75
還有這2個引數關於cms的,-XX:+UseCMSInitiatingOccupancyOnly表示JVM不基於執行時收集的資料來啟動CMS垃圾收集週期透過CMSInitiatingOccupancyFraction的值進行每一次CMS收集,-XX:CMSInitiatingOccupancyFraction=75 表示當老年代的使用率達到閾值75%時會觸發CMS GC。
備註:jstat -gcutil可以看出上圖的老年代的使用率才60.02%
還有最後一個引數解釋:
-XX:+UseParNewGC
-XX:+UseParNewGC 表示新生代使用ParNew並行收集器,那麼老年代呢? 可以讓同樣引數修改程式碼執行一次old gc即可看日誌有類似[Tenured:說明老年代使用的是Serial Old
備註:Serial Old使用的是標記壓縮演演算法。
解題
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
byte[] all1 = new byte[2 * _1MB];
byte[] all2 = new byte[2 * _1MB];
byte[] all3 = new byte[2 * _1MB];
byte[] all4 = new byte[7 * _1MB];
System.in.read();
}
說明:最後System.in.read();這句可以忽略,只是為了讓程式阻塞在那裡,不結束,這樣好看日誌,好看現象而已。
聰明如你一下子應該可以看到問題本質:同一份程式碼,jvm引數堆設定啥的都一樣,年輕代gc引數也一樣,唯一不同的就在於老年代gc使用上面,而jstat -gcutil圖表中FGC沒變的應該是正常結果,變了的CMS那個就是意外結果,所以關鍵點就在CMS上面了。
先來說說all1 、all12、all3、物件實體化開闢空間之後,eden空間都夠,他們都在eden空間中,當all4過來的時候,eden空間不夠了,需要執行ygc了。 下麵有2個問題需要說明,1、如果s0能存的下,可以看看JVM菜鳥進階高手之路三:MaxTenuringThreshold新生代的物件正常情況下最多經過多少次YGC的過程會晉升到老生代(CMS情況下預設為6),說到這裡可能還需要提一個引數:-XX:TargetSurvivorRatio,可以參考飛哥的:JVM Survivor行為一探究竟(http://www.jianshu.com/p/f91fde4628a5) 2、如果s0存不下,就是我們這裡的情況(由於我們這裡s0就是1M而已)所以直接進入到old空間了,所以可以看出來jstat -gcutil 裡面的老年代的比例都是60%幾了吧。 ygc執行完成之後,all4就還可以在eden分配(空間夠),所以可以看出來jstat -gcutil 裡面的eden的比例都是89%幾了吧
備註:-XX:PretenureSizeThreshold引數來設定多大的物件直接進入老年代(這個引數其實只對序列回收器和ParNew有效,對ParallelGC無效)。
如果是-Xmx20m -Xms20m -Xmn10m -XX:+UseParNewGC 這套引數,那麼結果就是如圖可以解釋了,並且每個引數比例啥的都可以理解了。
下麵來好好解釋下這個現象:
聰明如你一下子應該可以看到一個問題,那麼就是時間間隔是每隔2s執行一次,沒錯就是2s執行一次。需要說道-XX:CMSWaitDuration(Time in milliseconds that CMS thread waits for young GC)預設值是2s,我們修改為-XX:CMSWaitDuration=5000看看效果:
看到了吧,修改為5s就是5s執行一次變化了。那麼至於為什麼會執行呢??
本題就是當前新生代的物件是否能夠全部順利的晉升到老年代,如果不能,會觸發CMS GC。
具體可以看看我比較崇拜的狼哥的分析,一個比我牛逼並且比我努力的大牛。一個有意思的頻繁CMS問題。
本人其他JVM菜鳥進階高手之路相關文章或者其他系列文章可以關註公眾號【匠心零度】獲取更多!!!