1. 程式人生 > >JVM 優化經驗總結

JVM 優化經驗總結

開始之前

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,