1. 程式人生 > >JVM 優化經驗總結(原文已發表在IBM開發者論壇)

JVM 優化經驗總結(原文已發表在IBM開發者論壇)

插播小廣告,本人的《大話Java效能優化》一書已經在亞馬遜、京東、噹噹、天貓出售,提前謝謝大家支援。

開始之前

Java 虛擬機器有自己完善的硬體架構, 如處理器、堆疊、暫存器等,還具有相應的指令系統。JVM 遮蔽了與具體作業系統平臺相關的資訊,使得 Java 程式只需生成在 Java 虛擬機器上執行的目的碼 (位元組碼), 就可以在多種平臺上不加修改地執行。Java 虛擬機器在執行位元組碼時,實際上最終還是把位元組碼解釋成具體平臺上的機器指令執行。

注意:本文僅針對 JDK7、HotSPOT Java 虛擬機器,對於 JDK8 引入的 JVM 新特性及其他 Java 虛擬機器,本文不予關注。

我們以一個例子開始這篇文章。假設你是一個普通的 Java 物件,你出生在 Eden 區,在 Eden 區有許多和你差不多的小兄弟、小姐妹,可以把 Eden 區當成幼兒園,在這個幼兒園裡大家玩了很長時間。Eden 區不能無休止地放你們在裡面,所以當年紀稍大,你就要被送到學校去上學,這裡假設從小學到高中都稱為 Survivor 區。開始的時候你在 Survivor 區裡面劃分出來的的“From”區,讀到高年級了,就進了 Survivor 區的“To”區,中間由於學習成績不穩定,還經常來回折騰。直到你 18 歲的時候,高中畢業了,該去社會上闖闖了。於是你就去了年老代,年老代裡面人也很多。在年老代裡,你生活了 20 年 (每次 GC 加一歲),最後壽終正寢,被 GC 回收。有一點沒有提,你在年老代遇到了一個同學,他的名字叫愛德華 (慕光之城裡的帥哥吸血鬼),他以及他的家族永遠不會死,那麼他們就生活在永生代。

之前的文章《JVM 垃圾回收器工作原理及使用例項介紹》中已經介紹過年輕代、年老代、永生代,本文主要講講如何運用這些區域,為系統性能提供更好的幫助。本文不再重複這些概念,直接進入主題。

如何將新物件預留在年輕代

眾所周知,由於 Full GC 的成本遠遠高於 Minor GC,因此某些情況下需要儘可能將物件分配在年輕代,這在很多情況下是一個明智的選擇。雖然在大部分情況下,JVM 會嘗試在 Eden 區分配物件,但是由於空間緊張等問題,很可能不得不將部分年輕物件提前向年老代壓縮。因此,在 JVM 引數調優時可以為應用程式分配一個合理的年輕代空間,以最大限度避免新物件直接進入年老代的情況發生。清單 1 所示程式碼嘗試分配 4MB 記憶體空間,觀察一下它的記憶體使用情況。

清單 1. 相同大小記憶體分配
public class PutInEden {
 public static void main(String[] args){
 byte[] b1,b2,b3,b4;//定義變數
 b1=new byte[1024*1024];//分配 1MB 堆空間,考察堆空間的使用情況
 b2=new byte[1024*1024];
 b3=new byte[1024*1024];
 b4=new byte[1024*1024];
 }
}

使用 JVM 引數-XX:+PrintGCDetails -Xmx20M -Xms20M 執行清單 1 所示程式碼,輸出如清單 2 所示。

清單 2. 清單 1 執行輸出
[GC [DefNew: 5504K->640K(6144K), 0.0114236 secs] 5504K->5352K(19840K), 
   0.0114595 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[GC [DefNew: 6144K->640K(6144K), 0.0131261 secs] 10856K->10782K(19840K),
0.0131612 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[GC [DefNew: 6144K->6144K(6144K), 0.0000170 secs][Tenured: 10142K->13695K(13696K),
0.1069249 secs] 16286K->15966K(19840K), [Perm : 376K->376K(12288K)],
0.1070058 secs] [Times: user=0.03 sys=0.00, real=0.11 secs] 
[Full GC [Tenured: 13695K->13695K(13696K), 0.0302067 secs] 19839K->19595K(19840K), 
[Perm : 376K->376K(12288K)], 0.0302635 secs] [Times: user=0.03 sys=0.00, real=0.03 secs] 
[Full GC [Tenured: 13695K->13695K(13696K), 0.0311986 secs] 19839K->19839K(19840K), 
[Perm : 376K->376K(12288K)], 0.0312515 secs] [Times: user=0.03 sys=0.00, real=0.03 secs] 
[Full GC [Tenured: 13695K->13695K(13696K), 0.0358821 secs] 19839K->19825K(19840K), 
[Perm : 376K->371K(12288K)], 0.0359315 secs] [Times: user=0.05 sys=0.00, real=0.05 secs] 
[Full GC [Tenured: 13695K->13695K(13696K), 0.0283080 secs] 19839K->19839K(19840K),
[Perm : 371K->371K(12288K)], 0.0283723 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[Full GC [Tenured: 13695K->13695K(13696K), 0.0284469 secs] 19839K->19839K(19840K),
[Perm : 371K->371K(12288K)], 0.0284990 secs] [Times: user=0.03 sys=0.00, real=0.03 secs] 
[Full GC [Tenured: 13695K->13695K(13696K), 0.0283005 secs] 19839K->19839K(19840K),
[Perm : 371K->371K(12288K)], 0.0283475 secs] [Times: user=0.03 sys=0.00, real=0.03 secs] 
[Full GC [Tenured: 13695K->13695K(13696K), 0.0287757 secs] 19839K->19839K(19840K),
[Perm : 371K->371K(12288K)], 0.0288294 secs] [Times: user=0.03 sys=0.00, real=0.03 secs] 
[Full GC [Tenured: 13695K->13695K(13696K), 0.0288219 secs] 19839K->19839K(19840K), 
[Perm : 371K->371K(12288K)], 0.0288709 secs] [Times: user=0.03 sys=0.00, real=0.03 secs] 
[Full GC [Tenured: 13695K->13695K(13696K), 0.0293071 secs] 19839K->19839K(19840K),
[Perm : 371K->371K(12288K)], 0.0293607 secs] [Times: user=0.03 sys=0.00, real=0.03 secs] 
[Full GC [Tenured: 13695K->13695K(13696K), 0.0356141 secs] 19839K->19838K(19840K),
[Perm : 371K->371K(12288K)], 0.0356654 secs] [Times: user=0.01 sys=0.00, real=0.03 secs] 
Heap
 def new generation total 6144K, used 6143K [0x35c10000, 0x362b0000, 0x362b0000)
 eden space 5504K, 100% used [0x35c10000, 0x36170000, 0x36170000)
 from space 640K, 99% used [0x36170000, 0x3620fc80, 0x36210000)
 to space 640K, 0% used [0x36210000, 0x36210000, 0x362b0000)
 tenured generation total 13696K, used 13695K [0x362b0000, 0x37010000, 0x37010000)
 the space 13696K, 99% used [0x362b0000, 0x3700fff8, 0x37010000, 0x37010000)
 compacting perm gen total 12288K, used 371K [0x37010000, 0x37c10000, 0x3b010000)
 the space 12288K, 3% used [0x37010000, 0x3706cd20, 0x3706ce00, 0x37c10000)
 ro space 10240K, 51% used [0x3b010000, 0x3b543000, 0x3b543000, 0x3ba10000)
 rw space 12288K, 55% used [0x3ba10000, 0x3c0ae4f8, 0x3c0ae600, 0x3c610000)

清單 2 所示的日誌輸出顯示年輕代 Eden 的大小有 5MB 左右。分配足夠大的年輕代空間,使用 JVM 引數-XX:+PrintGCDetails -Xmx20M -Xms20M-Xmn6M 執行清單 1 所示程式碼,輸出如清單 3 所示。

清單 3. 增大 Eden 大小後清單 1 執行輸出
[GC [DefNew: 4992K->576K(5568K), 0.0116036 secs] 4992K->4829K(19904K), 
 0.0116439 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[GC [DefNew: 5568K->576K(5568K), 0.0130929 secs] 9821K->9653K(19904K), 
0.0131336 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
[GC [DefNew: 5568K->575K(5568K), 0.0154148 secs] 14645K->14500K(19904K),
0.0154531 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] 
[GC [DefNew: 5567K->5567K(5568K), 0.0000197 secs][Tenured: 13924K->14335K(14336K),
0.0330724 secs] 19492K->19265K(19904K), [Perm : 376K->376K(12288K)],
0.0331624 secs] [Times: user=0.03 sys=0.00, real=0.03 secs] 
[Full GC [Tenured: 14335K->14335K(14336K), 0.0292459 secs] 19903K->19902K(19904K),
[Perm : 376K->376K(12288K)], 0.0293000 secs] [Times: user=0.03 sys=0.00, real=0.03 secs] 
[Full GC [Tenured: 14335K->14335K(14336K), 0.0278675 secs] 19903K->19903K(19904K),
[Perm : 376K->376K(12288K)], 0.0279215 secs] [Times: user=0.03 sys=0.00, real=0.03 secs] 
[Full GC [Tenured: 14335K->14335K(14336K), 0.0348408 secs] 19903K->19889K(19904K),
[Perm : 376K->371K(12288K)], 0.0348945 secs] [Times: user=0.05 sys=0.00, real=0.05 secs] 
[Full GC [Tenured: 14335K->14335K(14336K), 0.0299813 secs] 19903K->19903K(19904K),
[Perm : 371K->371K(12288K)], 0.0300349 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 
[Full GC [Tenured: 14335K->14335K(14336K), 0.0298178 secs] 19903K->19903K(19904K),
[Perm : 371K->371K(12288K)], 0.0298688 secs] [Times: user=0.03 sys=0.00, real=0.03 secs] 
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space[Full GC [Tenured: 
14335K->14335K(14336K), 0.0294953 secs] 19903K->19903K(19904K),
[Perm : 371K->371K(12288K)], 0.0295474 secs] [Times: user=0.03 sys=0.00, real=0.03 secs] 
[Full GC [Tenured
: 14335K->14335K(14336K), 0.0287742 secs] 19903K->19903K(19904K), 
[Perm : 371K->371K(12288K)], 0.0288239 secs] [Times: user=0.03 sys=0.00, real=0.03 secs] 
[Full GC [Tenuredat GCTimeTest.main(GCTimeTest.java:16)
: 14335K->14335K(14336K), 0.0287102 secs] 19903K->19903K(19904K),
[Perm : 371K->371K(12288K)], 0.0287627 secs] [Times: user=0.03 sys=0.00, real=0.03 secs] 
Heap
 def new generation total 5568K, used 5567K [0x35c10000, 0x36210000, 0x36210000)
 eden space 4992K, 100% used [0x35c10000, 0x360f0000, 0x360f0000)
 from space 576K, 99% used [0x36180000, 0x3620ffe8, 0x36210000)
 to space 576K, 0% used [0x360f0000, 0x360f0000, 0x36180000)
 tenured generation total 14336K, used 14335K [0x36210000, 0x37010000, 0x37010000)
 the space 14336K, 99% used [0x36210000, 0x3700ffd8, 0x37010000, 0x37010000)
 compacting perm gen total 12288K, used 371K [0x37010000, 0x37c10000, 0x3b010000)
 the space 12288K, 3% used [0x37010000, 0x3706ce28, 0x3706d000, 0x37c10000)
 ro space 10240K, 51% used [0x3b010000, 0x3b543000, 0x3b543000, 0x3ba10000)
 rw space 12288K, 55% used [0x3ba10000, 0x3c0ae4f8, 0x3c0ae600, 0x3c610000)

通過清單 2 和清單 3 對比,可以發現通過設定一個較大的年輕代預留新物件,設定合理的 Survivor 區並且提供 Survivor 區的使用率,可以將年輕物件儲存在年輕代。一般來說,Survivor 區的空間不夠,或者佔用量達到 50%時,就會使物件進入年老代 (不管它的年齡有多大)。清單 4 建立了 3 個物件,分別分配一定的記憶體空間。

清單 4. 不同大小記憶體分配
public class PutInEden2 {
 public static void main(String[] args){
 byte[] b1,b2,b3;
 b1=new byte[1024*512];//分配 0.5MB 堆空間
 b2=new byte[1024*1024*4];//分配 4MB 堆空間
 b3=new byte[1024*1024*4];
 b3=null; //使 b3 可以被回收
 b3=new byte[1024*1024*4];//分配 4MB 堆空間
 }
}

使用引數-XX:+PrintGCDetails -Xmx1000M -Xms500M -Xmn100M -XX:SurvivorRatio=8 執行清單 4 所示程式碼,輸出如清單 5 所示。

清單 5. 清單 4 執行輸出
Heap
 def new generation total 92160K, used 11878K [0x0f010000, 0x15410000, 0x15410000)
 eden space 81920K, 2% used [0x0f010000, 0x0f1a9a20, 0x14010000)
 from space 10240K, 99% used [0x14a10000, 0x1540fff8, 0x15410000)
 to space 10240K, 0% used [0x14010000, 0x14010000, 0x14a10000)
 tenured generation total 409600K, used 86434K [0x15410000, 0x2e410000, 0x4d810000)
 the space 409600K, 21% used [0x15410000, 0x1a878b18, 0x1a878c00, 0x2e410000)
 compacting perm gen total 12288K, used 2062K [0x4d810000, 0x4e410000, 0x51810000)
 the space 12288K, 16% used [0x4d810000, 0x4da13b18, 0x4da13c00, 0x4e410000)
No shared spaces configured.

清單 5 輸出的日誌顯示,年輕代分配了 8M,年老代也分配了 8M。我們可以嘗試加上-XX:TargetSurvivorRatio=90 引數,這樣可以提高 from 區的利用率,使 from 區使用到 90%時,再將物件送入年老代,執行清單 4 程式碼,輸出如清單 6 所示。

清單 6. 修改執行引數後清單 4 輸出
Heap
 def new generation total 9216K, used 9215K [0x35c10000, 0x36610000, 0x36610000)
 eden space 8192K, 100% used [0x35c10000, 0x36410000, 0x36410000)
 from space 1024K, 99% used [0x36510000, 0x3660fc50, 0x36610000)
 to space 1024K, 0% used [0x36410000, 0x36410000, 0x36510000)
 tenured generation total 10240K, used 10239K [0x36610000, 0x37010000, 0x37010000)
 the space 10240K, 99% used [0x36610000, 0x3700ff70, 0x37010000, 0x37010000)
 compacting perm gen total 12288K, used 371K [0x37010000, 0x37c10000, 0x3b010000)
 the space 12288K, 3% used [0x37010000, 0x3706cd90, 0x3706ce00, 0x37c10000)
 ro space 10240K, 51% used [0x3b010000, 0x3b543000, 0x3b543000, 0x3ba10000)
 rw space 12288K, 55% used [0x3ba10000, 0x3c0ae4f8, 0x3c0ae600, 0x3c610000)

如果將 SurvivorRatio 設定為 2,將 b1 物件預存在年輕代。輸出如清單 7 所示。

清單 7. 再次修改執行引數後清單 4 輸出
Heap
 def new generation total 7680K, used 7679K [0x35c10000, 0x36610000, 0x36610000)
 eden space 5120K, 100% used [0x35c10000, 0x36110000, 0x36110000)
 from space 2560K, 99% used [0x36110000, 0x3638fff0, 0x36390000)
 to space 2560K, 0% used [0x36390000, 0x36390000, 0x36610000)
 tenured generation total 10240K, used 10239K [0x36610000, 0x37010000, 0x37010000)
 the space 10240K, 99% used [0x36610000, 0x3700fff0, 0x37010000, 0x37010000)
 compacting perm gen total 12288K, used 371K [0x37010000, 0x37c10000, 0x3b010000)
 the space 12288K, 3% used [0x37010000, 0x3706ce28, 0x3706d000, 0x37c10000)
 ro space 10240K, 51% used [0x3b010000, 0x3b543000, 0x3b543000, 0x3ba10000)
rw space 12288K, 55% used [0x3ba10000, 0x3c0ae4f8, 0x3c0ae600, 0x3c610000)

如何讓大物件進入年老代

我們在大部分情況下都會選擇將物件分配在年輕代。但是,對於佔用記憶體較多的大物件而言,它的選擇可能就不是這樣的。因為大物件出現在年輕代很可能擾亂年輕代 GC,並破壞年輕代原有的物件結構。因為嘗試在年輕代分配大物件,很可能導致空間不足,為了有足夠的空間容納大物件,JVM 不得不將年輕代中的年輕物件挪到年老代。因為大物件佔用空間多,所以可能需要移動大量小的年輕物件進入年老代,這對 GC 相當不利。基於以上原因,可以將大物件直接分配到年老代,保持年輕代物件結構的完整性,這樣可以提高 GC 的效率。如果一個大物件同時又是一個短命的物件,假設這種情況出現很頻繁,那對於 GC 來說會是一場災難。原本應該用於存放永久物件的年老代,被短命的物件塞滿,這也意味著對堆空間進行了洗牌,擾亂了分代記憶體回收的基本思路。因此,在軟體開發過程中,應該儘可能避免使用短命的大物件。可以使用引數-XX:PetenureSizeThreshold 設定大物件直接進入年老代的閾值。當物件的大小超過這個值時,將直接在年老代分配。引數-XX:PetenureSizeThreshold 只對序列收集器和年輕代並行收集器有效,並行回收收集器不識別這個引數。

清單 8. 建立一個大物件
public class BigObj2Old {
 public static void main(String[] args){
 byte[] b;
 b = new byte[1024*1024];//分配一個 1MB 的物件
 }
}

使用 JVM 引數-XX:+PrintGCDetails –Xmx20M –Xms20MB 執行,可以得到清單 9 所示日誌輸出。

清單 9. 清單 8 執行輸出
Heap
 def new generation total 6144K, used 1378K [0x35c10000, 0x362b0000, 0x362b0000)
 eden space 5504K, 25% used [0x35c10000, 0x35d689e8, 0x36170000)
 from space 640K, 0% used [0x36170000, 0x36170000, 0x36210000)
 to space 640K, 0% used [0x36210000, 0x36210000, 0x362b0000)
 tenured generation total 13696K, used 0K [0x362b0000, 0x37010000, 0x37010000)
 the space 13696K, 0% used [0x362b0000, 0x362b0000, 0x362b0200, 0x37010000)
 compacting perm gen total 12288K, used 374K [0x37010000, 0x37c10000, 0x3b010000)
 the space 12288K, 3% used [0x37010000, 0x3706dac8, 0x3706dc00, 0x37c10000)
 ro space 10240K, 51% used [0x3b010000, 0x3b543000, 0x3b543000, 0x3ba10000)
 rw space 12288K, 55% used [0x3ba10000, 0x3c0ae4f8, 0x3c0ae600, 0x3c610000)

可以看到該物件被分配在了年輕代,佔用了 25%的空間。如果需要將 1MB 以上的物件直接在年老代分配,設定-XX:PetenureSizeThreshold=1000000,程式執行後輸出如清單 10 所示。

清單 10. 修改執行引數後清單 8 輸出
Heap
 def new generation total 6144K, used 354K [0x35c10000, 0x362b0000, 0x362b0000)
 eden space 5504K, 6% used [0x35c10000, 0x35c689d8, 0x36170000)
 from space 640K, 0% used [0x36170000, 0x36170000, 0x36210000)
 to space 640K, 0% used [0x36210000, 0x36210000, 0x362b0000)
 tenured generation total 13696K, used 1024K [0x362b0000, 0x37010000, 0x37010000)
 the space 13696K, 7% used [0x362b0000, 0x363b0010, 0x363b0200, 0x37010000)
 compacting perm gen total 12288K, used 374K [0x37010000, 0x37c10000, 0x3b010000)
 the space 12288K, 3% used [0x37010000, 0x3706dac8, 0x3706dc00, 0x37c10000)
 ro space 10240K, 51% used [0x3b010000, 0x3b543000, 0x3b543000, 0x3ba10000)
 rw space 12288K, 55% used [0x3ba10000, 0x3c0ae4f8, 0x3c0ae600, 0x3c610000)

清單 10 裡面可以看到當滿 1MB 時進入到了年老代。

如何設定物件進入年老代的年齡

堆中的每一個物件都有自己的年齡。一般情況下,年輕物件存放在年輕代,年老物件存放在年老代。為了做到這點,虛擬機器為每個物件都維護一個年齡。如果物件在 Eden 區,經過一次 GC 後依然存活,則被移動到 Survivor 區中,物件年齡加 1。以後,如果物件每經過一次 GC 依然存活,則年齡再加 1。當物件年齡達到閾值時,就移入年老代,成為老年物件。這個閾值的最大值可以通過引數-XX:MaxTenuringThreshold 來設定,預設值是 15。雖然-XX:MaxTenuringThreshold 的值可能是 15 或者更大,但這不意味著新物件非要達到這個年齡才能進入年老代。事實上,物件實際進入年老代的年齡是虛擬機器在執行時根據記憶體使用情況動態計算的,這個引數指定的是閾值年齡的最大值。即,實際晉升年老代年齡等於動態計算所得的年齡與-XX:MaxTenuringThreshold 中較小的那個。清單 11 所示程式碼為 3 個物件申請了若干記憶體。

清單 11. 申請記憶體
public class MaxTenuringThreshold {
 public static void main(String args[]){
 byte[] b1,b2,b3;
 b1 = new byte[1024*512];
 b2 = new byte[1024*1024*2];
 b3 = new byte[1024*1024*4];
 b3 = null;
 b3 = new byte[1024*1024*4];
 }
}

引數設定為:-XX:+PrintGCDetails -Xmx20M -Xms20M -Xmn10M -XX:SurvivorRatio=2

執行清單 11 所示程式碼,輸出如清單 12 所示。

清單 12. 清單 11 執行輸出
[GC [DefNew: 2986K->690K(7680K), 0.0246816 secs] 2986K->2738K(17920K),
 0.0247226 secs] [Times: user=0.00 sys=0.02, real=0.03 secs] 
[GC [DefNew: 4786K->690K(7680K), 0.0016073 secs] 6834K->2738K(17920K), 
0.0016436 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation total 7680K, used 4888K [0x35c10000, 0x36610000, 0x36610000)
 eden space 5120K, 82% used [0x35c10000, 0x36029a18, 0x36110000)
 from space 2560K, 26% used [0x36110000, 0x361bc950, 0x36390000)
 to space 2560K, 0% used [0x36390000, 0x36390000, 0x36610000)
 tenured generation total 10240K, used 2048K [0x36610000, 0x37010000, 0x37010000)
 the space 10240K, 20% used [0x36610000, 0x36810010, 0x36810200, 0x37010000)
 compacting perm gen total 12288K, used 374K [0x37010000, 0x37c10000, 0x3b010000)
 the space 12288K, 3% used [0x37010000, 0x3706db50, 0x3706dc00, 0x37c10000)
 ro space 10240K, 51% used [0x3b010000, 0x3b543000, 0x3b543000, 0x3ba10000)
 rw space 12288K, 55% used [0x3ba10000, 0x3c0ae4f8, 0x3c0ae600, 0x3c610000)

更改引數為-XX:+PrintGCDetails -Xmx20M -Xms20M -Xmn10M -XX:SurvivorRatio=2 -XX:MaxTenuringThreshold=1,執行清單 11 所示程式碼,輸出如清單 13 所示。

清單 13. 修改執行引數後清單 11 輸出
[GC [DefNew: 2986K->690K(7680K), 0.0047778 secs] 2986K->2738K(17920K),
 0.0048161 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC [DefNew: 4888K->0K(7680K), 0.0016271 secs] 6936K->2738K(17920K),
0.0016630 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation total 7680K, used 4198K [0x35c10000, 0x36610000, 0x36610000)
 eden space 5120K, 82% used [0x35c10000, 0x36029a18, 0x36110000)
 from space 2560K, 0% used [0x36110000, 0x36110088, 0x36390000)
 to space 2560K, 0% used [0x36390000, 0x36390000, 0x36610000)
 tenured generation total 10240K, used 2738K [0x36610000, 0x37010000, 0x37010000)
 the space 10240K, 26% used [0x36610000, 0x368bc890, 0x368bca00, 0x37010000)
 compacting perm gen total 12288K, used 374K [0x37010000, 0x37c10000, 0x3b010000)
 the space 12288K, 3% used [0x37010000, 0x3706db50, 0x3706dc00, 0x37c10000)
 ro space 10240K, 51% used [0x3b010000, 0x3b543000, 0x3b543000, 0x3ba10000)
 rw space 12288K, 55% used [0x3ba10000, 0x3c0ae4f8, 0x3c0ae600, 0x3c610000)

清單 13 所示,第一次執行時 b1 物件在程式結束後依然儲存在年輕代。第二次執行前,我們減小了物件晉升年老代的年齡,設定為 1。即,所有經過一次 GC 的物件都可以直接進入年老代。程式執行後,可以發現 b1 物件已經被分配到年老代。如果希望物件儘可能長時間地停留在年輕代,可以設定一個較大的閾值。

穩定的 Java 堆 VS 動盪的 Java 堆

一般來說,穩定的堆大小對垃圾回收是有利的。獲得一個穩定的堆大小的方法是使-Xms 和-Xmx 的大小一致,即最大堆和最小堆 (初始堆) 一樣。如果這樣設定,系統在執行時堆大小理論上是恆定的,穩定的堆空間可以減少 GC 的次數。因此,很多服務端應用都會將最大堆和最小堆設定為相同的數值。但是,一個不穩定的堆並非毫無用處。穩定的堆大小雖然可以減少 GC 次數,但同時也增加了每次 GC 的時間。讓堆大小在一個區間中震盪,在系統不需要使用大記憶體時,壓縮堆空間,使 GC 應對一個較小的堆,可以加快單次 GC 的速度。基於這樣的考慮,JVM 還提供了兩個引數用於壓縮和擴充套件堆空間。

-XX:MinHeapFreeRatio 引數用來設定堆空間最小空閒比例,預設值是 40。當堆空間的空閒記憶體小於這個數值時,JVM 便會擴充套件堆空間。

-XX:MaxHeapFreeRatio 引數用來設定堆空間最大空閒比例,預設值是 70。當堆空間的空閒記憶體大於這個數值時,便會壓縮堆空間,得到一個較小的堆。

當-Xmx 和-Xms 相等時,-XX:MinHeapFreeRatio 和-XX:MaxHeapFreeRatio 兩個引數無效。

清單 14. 堆大小設定
import java.util.Vector;

public class HeapSize {
 public static void main(String args[]) throws InterruptedException{
 Vector v = new Vector();
 while(true){
 byte[] b = new byte[1024*1024];
 v.add(b);
 if(v.size() == 10){
 v = new Vector();
 }
 Thread.sleep(1);
 }
 }
}

清單 14 所示程式碼是測試-XX:MinHeapFreeRatio 和-XX:MaxHeapFreeRatio 的作用,設定執行引數為-XX:+PrintGCDetails -Xms10M -Xmx40M -XX:MinHeapFreeRatio=40 -XX:MaxHeapFreeRatio=50 時,輸出如清單 15 所示。

清單 15. 修改執行引數後清單 14 輸出
[GC [DefNew: 2418K->178K(3072K), 0.0034827 secs] 2418K->2226K(9920K),
 0.0035249 secs] [Times: user=0.00 sys=0.00, real=0.03 secs] 
[GC [DefNew: 2312K->0K(3072K), 0.0028263 secs] 4360K->4274K(9920K), 
0.0029905 secs] [Times: user=0.00 sys=0.00, real=0.03 secs] 
[GC [DefNew: 2068K->0K(3072K), 0.0024363 secs] 6342K->6322K(9920K),
0.0024836 secs] [Times: user=0.00 sys=0.00, real=0.03 secs] 
[GC [DefNew: 2061K->0K(3072K), 0.0017376 secs][Tenured: 8370K->8370K(8904K),
0.1392692 secs] 8384K->8370K(11976K), [Perm : 374K->374K(12288K)],
0.1411363 secs] [Times: user=0.00 sys=0.02, real=0.16 secs] 
[GC [DefNew: 5138K->0K(6336K), 0.0038237 secs] 13508K->13490K(20288K),
0.0038632 secs] [Times: user=0.00 sys=0.00, real=0.03 secs]

改用引數:-XX:+PrintGCDetails -Xms40M -Xmx40M -XX:MinHeapFreeRatio=40 -XX:MaxHeapFreeRatio=50,執行輸出如清單 16 所示。

清單 16. 再次修改執行引數後清單 14 輸出
[GC [DefNew: 10678K->178K(12288K), 0.0019448 secs] 10678K->178K(39616K), 
 0.0019851 secs] [Times: user=0.00 sys=0.00, real=0.03 secs] 
[GC [DefNew: 10751K->178K(12288K), 0.0010295 secs] 10751K->178K(39616K),
0.0010697 secs] [Times: user=0.00 sys=0.00, real=0.02 secs] 
[GC [DefNew: 10493K->178K(12288K), 0.0008301 secs] 10493K->178K(39616K),
0.0008672 secs] [Times: user=0.00 sys=0.00, real=0.02 secs] 
[GC [DefNew: 10467K->178K(12288K), 0.0008522 secs] 10467K->178K(39616K),
0.0008905 secs] [Times: user=0.00 sys=0.00, real=0.02 secs] 
[GC [DefNew: 10450K->178K(12288K), 0.0008964 secs] 10450K->178K(39616K),
0.0009339 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC [DefNew: 10439K->178K(12288K), 0.0009876 secs] 10439K->178K(39616K),
0.0010279 secs] [Times: user=0.00 sys=0.00, real=0.02 secs]

從清單 16 可以看出,此時堆空間的垃圾回收穩定在一個固定的範圍。在一個穩定的堆中,堆空間大小始終不變,每次 GC 時,都要應對一個 40MB 的空間。因此,雖然 GC 次數減小了,但是單次 GC 速度不如一個震盪的堆。

增大吞吐量提升系統性能

吞吐量優先的方案將會盡可能減少系統執行垃圾回收的總時間,故可以考慮關注系統吞吐量的並行回收收集器。在擁有高效能的計算機上,進行吞吐量優先優化,可以使用引數:

java –Xmx3800m –Xms3800m –Xmn2G –Xss128k –XX:+UseParallelGC 
   –XX:ParallelGC-Threads=20 –XX:+UseParallelOldGC

–Xmx380m –Xms3800m:設定 Java 堆的最大值和初始值。一般情況下,為了避免堆記憶體的頻繁震盪,導致系統性能下降,我們的做法是設定最大堆等於最小堆。假設這裡把最小堆減少為最大堆的一半,即 1900m,那麼 JVM 會盡可能在 1900MB 堆空間中執行,如果這樣,發生 GC 的可能性就會比較高;

-Xss128k:減少執行緒棧的大小,這樣可以使剩餘的系統記憶體支援更多的執行緒;

-Xmn2g:設定年輕代區域大小為 2GB;

–XX:+UseParallelGC:年輕代使用並行垃圾回收收集器。這是一個關注吞吐量的收集器,可以儘可能地減少 GC 時間。

–XX:ParallelGC-Threads:設定用於垃圾回收的執行緒數,通常情況下,可以設定和 CPU 數量相等。但在 CPU 數量比較多的情況下,設定相對較小的數值也是合理的;

–XX:+UseParallelOldGC:設定年老代使用並行回收收集器。

嘗試使用大的記憶體分頁

CPU 是通過定址來訪問記憶體的。32 位 CPU 的定址寬度是 0~0xFFFFFFFF ,計算後得到的大小是 4G,也就是說可支援的實體記憶體最大是 4G。但在實踐過程中,碰到了這樣的問題,程式需要使用 4G 記憶體,而可用實體記憶體小於 4G,導致程式不得不降低記憶體佔用。為了解決此類問題,現代 CPU 引入了 MMU(Memory Management Unit 記憶體管理單元)。MMU 的核心思想是利用虛擬地址替代實體地址,即 CPU 定址時使用虛址,由 MMU 負責將虛址對映為實體地址。MMU 的引入,解決了對實體記憶體的限制,對程式來說,就像自己在使用 4G 記憶體一樣。記憶體分頁 (Paging) 是在使用 MMU 的基礎上,提出的一種記憶體管理機制。它將虛擬地址和實體地址按固定大小(4K)分割成頁 (page) 和頁幀 (page frame),並保證頁與頁幀的大小相同。這種機制,從資料結構上,保證了訪問記憶體的高效,並使 OS 能支援非連續性的記憶體分配。在程式記憶體不夠用時,還可以將不常用的實體記憶體頁轉移到其他儲存裝置上,比如磁碟,這就是大家耳熟能詳的虛擬記憶體。

在 Solaris 系統中,JVM 可以支援 Large Page Size 的使用。使用大的記憶體分頁可以增強 CPU 的記憶體定址能力,從而提升系統的效能。

java –Xmx2506m –Xms2506m –Xmn1536m –Xss128k –XX:++UseParallelGC
 –XX:ParallelGCThreads=20 –XX:+UseParallelOldGC –XX:+LargePageSizeInBytes=256m

–XX:+LargePageSizeInBytes:設定大頁的大小。

過大的記憶體分頁會導致 JVM 在計算 Heap 內部分割槽(perm, new, old)記憶體佔用比例時,會出現超出正常值的劃分,最壞情況下某個區會多佔用一個頁的大小。

使用非佔有的垃圾回收器

為降低應用軟體的垃圾回收時的停頓,首先考慮的是使用關注系統停頓的 CMS 回收器,其次,為了減少 Full GC 次數,應儘可能將物件預留在年輕代,因為年輕代 Minor GC 的成本遠遠小於年老代的 Full GC。

java –Xmx3550m –Xms3550m –Xmn2g –Xss128k –XX:ParallelGCThreads=20
 –XX:+UseConcMarkSweepGC –XX:+UseParNewGC –XX:+SurvivorRatio=8 –XX:TargetSurvivorRatio=90
 –XX:MaxTenuringThreshold=31

–XX:ParallelGCThreads=20:設定 20 個執行緒進行垃圾回收;

–XX:+UseParNewGC:年輕代使用並行回收器;

–XX:+UseConcMarkSweepGC:年老代使用 CMS 收集器降低停頓;

–XX:+SurvivorRatio:設定 Eden 區和 Survivor 區的比例為 8:1。稍大的 Survivor 空間可以提高在年輕代回收生命週期較短的物件的可能性,如果 Survivor 不夠大,一些短命的物件可能直接進入年老代,這對系統來說是不利的。

–XX:TargetSurvivorRatio=90:設定 Survivor 區的可使用率。這裡設定為 90%,則允許 90%的 Survivor 空間被使用。預設值是 50%。故該設定提高了 Survivor 區的使用率。當存放的物件超過這個百分比,則物件會向年老代壓縮。因此,這個選項更有助於將物件留在年輕代。

–XX:MaxTenuringThreshold:設定年輕物件晉升到年老代的年齡。預設值是 15 次,即物件經過 15 次 Minor GC 依然存活,則進入年老代。這裡設定為 31,目的是讓物件儘可能地儲存在年輕代區域。

結束語

通過本文的學習,讀者瞭解瞭如何將新物件預留在年輕代、如何讓大物件進入年老代、如何設定物件進入年老代的年齡、穩定的 Java 堆 VS 動盪的 Java 堆、增大吞吐量提升系統性能、嘗試使用大的記憶體分頁、使用非佔有的垃圾回收器等主題,通過例項及對應輸出解釋的形式讓讀者對於 JVM 優化有一個初步認識。如其他文章相同的觀點,沒有哪一條優化是固定不變的,讀者需要自己判斷、實踐後才能找到正確的道路。

相關推薦

JVM 優化經驗總結(原文發表IBM開發者論壇)

插播小廣告,本人的《大話Java效能優化》一書已經在亞馬遜、京東、噹噹、天貓出售,提前謝謝大家支援。 開始之前 Java 虛擬機器有自己完善的硬體架構, 如處理器、堆疊、暫存器等,還具有相應的指令系統。JVM 遮蔽了與具體作業系統平臺相關的資訊,使得 Java 程式只需

JVM 優化經驗總結

開始之前 Java 虛擬機器有自己完善的硬體架構, 如處理器、堆疊、暫存器等,還具有相應的指令系統。JVM 遮蔽了與具體作業系統平臺相關的資訊,使得 Java 程式只需生成在 Java 虛擬機器上執行的目的碼 (位元組碼), 就可以在多種平臺上不加修改地執行。Java 虛擬機器在執行位元組碼時,實際上最

Java集合類操作優化經驗總結

設置 mar ise long 初始化 實際類型 線性表 core 不一定 在實際的項目開發中會有非常多的對象,怎樣高效、方便地管理對象,成為影響程序性能與可維護性的重要環節。Java 提供了集合框架來解決此類問題。線性表、鏈表、哈希表等是經常使用的數據結構,在

Hbase常用優化、Hbae效能優化、Hbase優化經驗總結

Hbase優化 1.預先分割槽 預設情況下,在建立 HBase 表的時候會自動建立一個 Region 分割槽,當匯入資料的時候,所有的 HBase 客戶端都向這一個 Region 寫資料,直到這個 Region 足夠大了才進行切分。一種可以加快批量寫入速度的方法是通過預先建立一些空的 Reg

WPF效能優化經驗總結

原文地址:https://www.cnblogs.com/chiniao/archive/2010/08/09/1795499.html WPF效能優化一、Rendering Tier 1. 根據硬體配置的不同,WPF採用不同的Rendering Tier做渲染。下列情況請特別注意,因為在這

SQL優化經驗總結34條

我們要做到不但會寫SQL,還要做到寫出效能優良的SQL,以下為筆者學習、摘錄、並彙總部分資料與大家分享! 我們要做到不但會寫SQL,還要做到寫出效能優良的SQL,以下為筆者學習、摘錄、並彙總部分資料與大家分享! (1) 選擇最有效率的表名順序(只在基於規則的優

SQL優化經驗總結

(1) 選擇最有效率的表名順序(只在基於規則的優化器中有效): ORACLE 的解析器按照從右到左的順序處理FROM子句中的表名,FROM子句中寫在最後的表(基礎表 driving table)將被最先處理,在FROM子句中包含多個表的情況下,你必須選擇記錄條數最少的表作為

Lucene底層原理和優化經驗分享(2)-Lucene優化經驗總結

  系統優化遵從木桶原理:一隻木桶能盛多少水,並不取決於最高的木板,而取決於最短的那塊木板。Lucene優化也一樣,找到效能瓶頸,找對解決方法,才能事半功倍,本文將從三方面闡述我們的Lucene優化經驗:   1. 找準方向 -> Lucene效能瓶頸分

資料庫效能優化經驗總結

1.資料庫訪問優化法則 要正確的優化SQL,我們需要快速定位能性的瓶頸點,也就是說快速找到我們SQL主要的開銷在哪裡?而大多數情況效能最慢的裝置會是瓶頸點,如下載時網路速度可能會是瓶頸點,本地複製檔案時硬碟可能會是瓶頸點,為什麼這些一般的工作我們能快速確認瓶頸點呢,因為

第二節 JVM優化應用以及知識總結

family initial 內存 -s window tom linu ast 類型 在JVM中。假設98%的時間是用於GC且可用的HeapSize不足2%時將會拋出OOM異常;HeapSize最大不要超過可用物理內存的80%,一般-Xms –Xmx設置為同樣,-

Tomcat調優總結(Tomcat自身優化、Linux內核優化JVM優化

函數 基礎 order git sed .config cycle 運算 mage Tomcat自身的調優是針對conf/server.xml中的幾個參數的調優設置。首先是對這幾個參數的含義要有深刻而清楚的理解。以tomcat8.5為例,講解參數。 同時也得認識到一點,to

一位10年Java程式設計師總結進階中的你懂多執行緒和jvm優化嗎?

感謝朋友們的認可和指正。本文是有感而發,因為看過了太多坑人的部落格和書籍,感慨自己走過的彎路,不希望其他初學者被網上互相抄襲的部落格和東拼西湊的書籍浪費時間,想以一個相對巨集觀的視野來描述一個概念,力求通俗易懂,所以沒有深入太多細節,簡化了很多模型,給部分朋友造成了疑惑,說聲抱歉。也沒有配圖,都是抽

dashBoard 經驗總結 --DBD 失真問題的優化

關於 DBD 失真問題的優化: 首先判斷是否滿足一屏顯示: 一、如果滿足一屏顯示,預設展現 DBD 的 jsp 是寬高自適應的, 這種情況下為保證各元件不失真操作方式有如下 2 種: 1. 確定各元件佔 DBD 區域的大小,然後報表紙張也指定為同樣的大小,這樣調整後 各

JVM優化知識點升級和配置總結

一、JVM升級:      ① Sun公司的HotSpot;      ② BEA公司的JRockit;      ③ IBM公司的J9 JVM; 在JDK1.7及其以前我們所使用的都是Sun公司的HotSpot,但由於Sun公司和BEA公司都被oracle收購,jd

基於Metronic的Bootstrap開發框架經驗總結(6)--對話方塊及提示框的處理和優化

在各種Web開發過程中,對話方塊和提示框的處理是很常見的一種介面處理技術,用得好,可以給使用者很好的頁面體驗,Bootstrap開發也一樣,我們往往在頁面新增、編輯、檢視詳細等介面使用彈出對話方塊層的方式進行顯示資料,刪除則可能使用一個提示確認框,如果操作成功,我們可以使用更豐富的提示框來處理,本篇主要對比說

基於Metronic的Bootstrap開發框架經驗總結(10)--優化Bootstrap圖示管理

在基於Bootstrap開發的專案中,鮮豔顏色的按鈕,以及豐富的圖表是很吸引人的特點,為了將這個特點發揮到極致,可以利用Bootstrap圖示抽取到資料庫裡面,並在介面中進行管理和使用,這樣我們可以把這些圖示方便應用在各個頁面部件上,如選單模組,按鈕介面,表單輸入等多個場合進行使用。在前面隨筆《基於Metro

網際網路“大佬”總結優化經驗 是否可信任

  做慣了SEO優化、看慣了所謂磚家的言論,從事優化的站長們多多少少會受到別人的影響,過於緊張的從理論中找答案,但實際的操作中,理論的知識又能相信多少?常見的一些優化因素到底對關鍵詞排名有和影響,例項給大家分析下小編自己看到的真實。   關鍵詞密度   站長們都對關鍵字的密

Android4.0開機啟動速度優化過程經驗總結

      筆者EDE101使用的平臺是:A10+android4.0.4+16GB NAND+1G DDR+(1280 X RGB X 800)。專案進入收尾階段,需要著重解決一下開機速度的問題,客戶反映還是太慢了一點,當然我們得列入高優先順序重點攻關解決了。優化開機速度前,平均android啟動速度大概在

Oracle資料庫優化經驗總結

個人理解,資料庫效能最關鍵的因素在於IO,因為操作記憶體是快速的,但是讀寫磁碟是速度很慢的,優化資料庫最關鍵的問題在於減少磁碟的IO,就個 人理解應該分為物理的和邏輯的優化, 物理的是指oracle產品本身的一些優化,邏輯優化是指應用程式級別的優化物理優化的一些原則:  1)Oracle的執行環境(網路,硬體

關於ios稽核被拒3.2.1問題成功過審經驗總結

蘋果稽核依據的稽核標準有2個,一個是《App Store 稽核指南》,一個是《蘋果開發者計劃許可協議》。 最近好多朋友問我因金融許可證被拒後是怎麼過審的,我們都知道 從17年開始 .蘋果對APP的稽核越來越嚴格,同時稽核依據越來越緊跟國內的政策走,比如國家對金