Java虛擬機器詳解----常用JVM配置引數
原文地址:http://www.cnblogs.com/smyhvae/p/4736162.html
本文主要內容:
- Trace跟蹤引數
- 堆的分配引數
- 棧的分配引數
零、在IDE的後臺列印GC日誌:
既然學習JVM,閱讀GC日誌是處理Java虛擬機器記憶體問題的基礎技能,它只是一些人為確定的規則,沒有太多技術含量。
既然如此,那麼在IDE的控制檯列印GC日誌是必不可少的了。現在就告訴你怎麼列印。
(1)如果你用的是Eclipse,列印GC日誌的操作如下:
在上圖的箭頭處加上-XX:+PrintGCDetails這句話。於是,執行程式後,GC日誌就可以打印出來了:
(2)如果你用的是IntelliJ IDEA,列印GC日誌的操作如下:
在上圖的箭頭處加上-XX:+PrintGCDetails這句話。於是,執行程式後,GC日誌就可以打印出來了:
當然了,光有-XX:+PrintGCDetails這一句引數肯定是不夠的,下面我們詳細介紹一下更多的引數配置。
一、Trace跟蹤引數:
1、列印GC的簡要資訊:
-verbose:gc
-XX:+printGC
解釋:可以列印GC的簡要資訊。比如:
[GC 4790K->374K(15872K), 0.0001606 secs]
[GC 4790K->374K(15872K), 0.0001474 secs]
[GC 4790K->374K(15872K), 0.0001563 secs]
[GC 4790K->374K(15872K), 0.0001682 secs]
上方日誌的意思是說,GC之前,用了4M左右的記憶體,GC之後,用了374K記憶體,一共回收了將近4M。記憶體大小一共是16M左右。
2、列印GC的詳細資訊:
-XX:+PrintGCDetails
解釋:列印GC詳細資訊。
-XX:+PrintGCTimeStamps
解釋:列印CG發生的時間戳。
理解GC日誌的含義:
例如下面這段日誌:
[GC[DefNew: 4416K->0K(4928K), 0.0001897 secs] 4790K->374K(15872K), 0.0002232 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
上方日誌的意思是說:這是一個新生代的GC。方括號內部的“4416K->0K(4928K)”含義是:“GC前該記憶體區域已使用容量->GC後該記憶體區域已使用容量(該記憶體區域總容量)”。而在方括號之外的“4790K->374K(15872K)”表示“GC前Java堆已使用容量->GC後Java堆已使用容量(Java堆總容量)”。
再往後看,“0.0001897 secs”表示該記憶體區域GC所佔用的時間,單位是秒。
再比如下面這段GC日誌:
上圖中,我們先看一下用紅框標註的“[0x27e80000, 0x28d80000, 0x28d80000)”的含義,它表示新生代在記憶體當中的位置:第一個引數是申請到的起始位置,第二個引數是申請到的終點位置,第三個引數表示最多能申請到的位置。上圖中的例子表示新生代申請到了15M的控制元件,而這個15M是等於:(eden space的12288K)+(from space的1536K)+(to space的1536K)。
疑問:分配到的新生代有15M,但是可用的只有13824K,為什麼會有這個差異呢?等我們在後面的文章中學習到了GC演算法之後就明白了。
3、指定GC log的位置:
-Xloggc:log/gc.log
解釋:指定GC log的位置,以檔案輸出。幫助開發人員分析問題。
-XX:+PrintHeapAtGC
解釋:每一次GC前和GC後,都列印堆資訊。
例如:
上圖中,紅框部分正好是一次GC,紅框部分的前面是GC之前的日誌,紅框部分的後面是GC之後的日誌。
-XX:+TraceClassLoading
解釋:監控類的載入。
例如:
[Loaded java.lang.Object from shared objects file]
[Loaded java.io.Serializable from shared objects file]
[Loaded java.lang.Comparable from shared objects file]
[Loaded java.lang.CharSequence from shared objects file]
[Loaded java.lang.String from shared objects file]
[Loaded java.lang.reflect.GenericDeclaration from shared objects file]
[Loaded java.lang.reflect.Type from shared objects file]
-XX:+PrintClassHistogram
解釋:按下Ctrl+Break後,列印類的資訊。
例如:
二、堆的分配引數:
1、-Xmx –Xms:指定最大堆和最小堆
舉例、當引數設定為如下時:
-Xmx20m -Xms5m
然後我們在程式中執行如下程式碼:
System.out.println("Xmx=" + Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M"); //系統的最大空間
System.out.println("free mem=" + Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M"); //系統的空閒空間
System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M"); //當前可用的總空間
執行效果:
保持引數不變,在程式中執行如下程式碼:(分配1M空間給陣列)
byte[] b = new byte[1 * 1024 * 1024]; System.out.println("分配了1M空間給陣列");
System.out.println("Xmx=" + Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M"); //系統的最大空間
System.out.println("free mem=" + Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M"); //系統的空閒空間
System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
執行效果:
注:Java會盡可能將total mem的值維持在最小堆。
保持引數不變,在程式中執行如下程式碼:(分配10M空間給陣列)
byte[] b = new byte[10 * 1024 * 1024]; System.out.println("分配了10M空間給陣列");
System.out.println("Xmx=" + Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M"); //系統的最大空間
System.out.println("free mem=" + Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M"); //系統的空閒空間
System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M"); //當前可用的總空間
執行效果:
如上圖紅框所示:此時,total mem 為7M時已經不能滿足需求了,於是total mem漲成了16.5M。
保持引數不變,在程式中執行如下程式碼:(進行一次GC的回收)
System.gc();
System.out.println("Xmx=" + Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M"); //系統的最大空間
System.out.println("free mem=" + Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M"); //系統的空閒空間
System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M"); //當前可用的總空間
執行效果:
問題1: -Xmx(最大堆空間)和 –Xms(最小堆空間)應該保持一個什麼關係,可以讓系統的效能儘可能的好呢?
問題2:如果你要做一個Java的桌面產品,需要繫結JRE,但是JRE又很大,你如何做一下JRE的瘦身呢?
2、-Xmn、-XX:NewRatio、-XX:SurvivorRatio:
- -Xmn
設定新生代大小
- -XX:NewRatio
新生代(eden+2*s)和老年代(不包含永久區)的比值
例如:4,表示新生代:老年代=1:4,即新生代佔整個堆的1/5
- -XX:SurvivorRatio(倖存代)
設定兩個Survivor區和eden的比值
例如:8,表示兩個Survivor:eden=2:8,即一個Survivor佔年輕代的1/10
現在執行如下這段程式碼:
public class JavaTest { public static void main(String[] args) { byte[] b = null; for (int i = 0; i < 10; i++) b = new byte[1 * 1024 * 1024]; } }
我們通過設定不同的jvm引數,來看一下GC日誌的區別。
(1)當引數設定為如下時:(設定新生代為1M,很小)
-Xmx20m -Xms20m -Xmn1m -XX:+PrintGCDetails
執行效果:
總結:
沒有觸發GC
由於新生代的記憶體比較小,所以全部分配在老年代。
(2)當引數設定為如下時:(設定新生代為15M,足夠大)
-Xmx20m -Xms20m -Xmn15m -XX:+PrintGCDetails
執行效果:
上圖顯示:
沒有觸發GC
全部分配在eden(藍框所示)
老年代沒有使用(紅框所示)
(3)當引數設定為如下時:(設定新生代為7M,不大不小)
-Xmx20m -Xms20m –Xmn7m -XX:+PrintGCDetails
執行效果:
總結:
進行了2次新生代GC
s0 s1 太小,需要老年代擔保
(4)當引數設定為如下時:(設定新生代為7M,不大不小;同時,增加倖存代大小)
-Xmx20m -Xms20m -Xmn7m -XX:SurvivorRatio=2 -XX:+PrintGCDetails
執行效果:
總結:
進行了至少3次新生代GC
s0 s1 增大
(5)當引數設定為如下時:
-Xmx20m -Xms20m -XX:NewRatio=1 -XX:SurvivorRatio=2 -XX:+PrintGCDetails
執行效果:
(6)當引數設定為如下時: 和上面的(5)相比,適當減小倖存代大小,這樣的話,能夠減少GC的次數
-Xmx20m -Xms20m -XX:NewRatio=1 -XX:SurvivorRatio=3 -XX:+PrintGCDetails
3、-XX:+HeapDumpOnOutOfMemoryError、-XX:+HeapDumpPath
- -XX:+HeapDumpOnOutOfMemoryError
OOM時匯出堆到檔案
根據這個檔案,我們可以看到系統dump時發生了什麼。
- -XX:+HeapDumpPath
匯出OOM的路徑
例如我們設定如下的引數:
-Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/a.dump
上方意思是說,現在給堆記憶體最多分配20M的空間。如果發生了OOM異常,那就把dump資訊匯出到d:/a.dump檔案中。
然後,我們執行如下程式碼:
Vector v = new Vector(); for (int i = 0; i < 25; i++) v.add(new byte[1 * 1024 * 1024]);
上方程式碼中,需要利用25M的空間,很顯然會發生OOM異常。現在我們執行程式,控制檯列印如下:
現在我們去D盤看一下dump檔案:
上圖顯示,一般來說,這個檔案的大小和最大堆的大小保持一致。
我們可以用VisualVM開啟這個dump檔案。
注:關於VisualVM的使用,可以參考下面這篇部落格:
或者使用Java自帶的Java VisualVM工具也行:
上圖中就是dump出來的檔案,檔案中可以看到,一共有19個byte已經被分配了。
4、-XX:OnOutOfMemoryError:
- -XX:OnOutOfMemoryError
在OOM時,執行一個指令碼。
可以在OOM時,傳送郵件,甚至是重啟程式。
例如我們設定如下的引數:
-XX:OnOutOfMemoryError=D:/tools/jdk1.7_40/bin/printstack.bat %p //p代表的是當前程序的pid
上方引數的意思是說,執行printstack.bat指令碼,而這個指令碼做的事情是:D:/tools/jdk1.7_40/bin/jstack -F %1 > D:/a.txt,即當程式OOM時,在D:/a.txt中將會生成執行緒的dump。
5、堆的分配引數總結:
- 根據實際事情調整新生代和倖存代的大小
- 官方推薦新生代佔堆的3/8
- 倖存代佔新生代的1/10
- 在OOM時,記得Dump出堆,確保可以排查現場問題
6、永久區分配引數:
- -XX:PermSize -XX:MaxPermSize
設定永久區的初始空間和最大空間。也就是說,jvm啟動時,永久區一開始就佔用了PermSize大小的空間,如果空間還不夠,可以繼續擴充套件,但是不能超過MaxPermSize,否則會OOM。
他們表示,一個系統可以容納多少個型別
程式碼舉例:
我們知道,使用CGLIB等庫的時候,可能會產生大量的類,這些類,有可能撐爆永久區導致OOM。於是,我們執行下面這段程式碼:
for(int i=0;i<100000;i++){ CglibBean bean = new CglibBean("geym.jvm.ch3.perm.bean"+i,new HashMap()); }
上面這段程式碼會在永久區不斷地產生新的類。於是,執行效果如下:
總結:
如果堆空間沒有用完也丟擲了OOM,有可能是永久區導致的。
堆空間實際佔用非常少,但是永久區溢位 一樣丟擲OOM。
三、棧的分配引數:
1、Xss:
設定棧空間的大小。通常只有幾百K
決定了函式呼叫的深度
每個執行緒都有獨立的棧空間
區域性變數、引數 分配在棧上
注:棧空間是每個執行緒私有的區域。棧裡面的主要內容是棧幀,而棧幀存放的是區域性變量表,區域性變量表的內容是:區域性變數、引數。
我們來看下面這段程式碼:(沒有出口的遞迴呼叫)
public class TestStackDeep { private static int count = 0;
public static void recursion(long a, long b, long c) { long e = 1, f = 2, g = 3, h = 4, i = 5, k = 6, q = 7, x = 8, y = 9, z = 10; count++; recursion(a, b, c); }
public static void main(String args[]) { try { recursion(0L, 0L, 0L); } catch (Throwable e) { System.out.println("deep of calling = " + count); e.printStackTrace(); } } }
上方這段程式碼是沒有出口的遞迴呼叫,肯定會出現OOM的。
如果設定棧大小為128k:
-Xss128K
執行效果如下:(方法被呼叫了294次)
如果設定棧大小為256k:(方法被呼叫748次)
意味著函式呼叫的次數太深,例如遞迴呼叫。
總結:
我們在本文中介紹了jvm的一些最基本的引數,還有很多引數(如GC引數等)將在後續的系列文章中進行介紹。我們將在接下來的文章中介紹GC演算法。