JVM菜鸟进阶高手之路
本文轉載自公眾號 ?匠心零度
問題現象
代碼如下,使用 ParNew + Serial Old 回收器組合與使用 ParNew + CMS 回收器組合時,結果為什么差異如此之大 ?
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。
—————END—————
總結
以上是生活随笔為你收集整理的JVM菜鸟进阶高手之路的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring 思维导图,让 Spring
- 下一篇: 漫画:什么是MapReduce