JVM效能優化入門指南

MountainKing發表於2016-03-16

前言

入門JVM垃圾回收機制後,接下來可以學習效能調優了。主要有兩部分內容:

  • JDK工具的使用。
  • 調優策略。

兵器譜

jps

列出正在執行的虛擬機器程式,用法如下:

jps [-option] [hostid]
選項 作用
q 只輸出LVMID,省略主類的名稱
m 輸出main method的引數
l 輸出完全的包名,應用主類名,jar的完全路徑名
v 輸出jvm引數

jstat

監視虛擬機器執行狀態資訊,使用方式:

jstat -<option> <pid> [interval[s|ms]]
選項 作用
gc 輸出每個堆區域的當前可用空間以及已用空間,GC執行的總次數,GC操作累計所花費的時間。
gccapactiy 輸出每個堆區域的最小空間限制(ms)/最大空間限制(mx),當前大小,每個區域之上執行GC的次數。(不輸出當前已用空間以及GC執行時間)。
gccause 輸出-gcutil提供的資訊以及最後一次執行GC的發生原因和當前所執行的GC的發生原因。
gcnew 輸出新生代空間的GC效能資料。
gcnewcapacity 輸出新生代空間的大小的統計資料。
gcold 輸出老年代空間的GC效能資料。
gcoldcapacity 輸出老年代空間的大小的統計資料。
gcpermcapacity 輸出持久帶空間的大小的統計資料。
gcutil 輸出每個堆區域使用佔比,以及GC執行的總次數和GC操作所花費的事件。

比如:

jstat -gc 28389 1s

每隔1秒輸出一次JVM執行資訊:

S0C    S1C    S0U    S1U      EC       EU        OC         OU       PC     PU    YGC     YGCT    FGC    FGCT     GCT   
52416.0 52416.0 4744.9  0.0   419456.0 28180.6  2621440.0   439372.6  131072.0 33564.8 160472 1760.603  61      2.731 1763.334
說明 jstat引數
S0C Survivor0空間的大小。單位KB。 -gc -gccapacity -gcnew -gcnewcapacity
S1C Survivor1空間的大小。單位KB。 -gc -gccapacity -gcnew -gcnewcapacity
S0U Survivor0已用空間的大小。單位KB。 -gc -gcnew
S1U Survivor1已用空間的大小。單位KB。 -gc -gcnew
EC Eden空間的大小。單位KB。 -gc -gccapacity -gcnew -gcnewcapacity
EU Eden已用空間的大小。單位KB。 -gc-gcnew
OC 老年代空間的大小。單位KB。 -gc -gccapacity -gcold -gcoldcapacity
OU 老年代已用空間的大小。單位KB。 -gc -gcold
PC 持久代空間的大小。單位KB。 -gc -gccapacity -gcold -gcoldcapacity -gcpermcapacity
PU 持久代已用空間的大小。單位KB。 -gc -gcold
YGC 新生代空間GC時間發生的次數。 -gc -gccapacity -gcnew -gcnewcapacity -gcold -gcoldcapacity -gcpermcapacity -gcutil -gccause
YGCT 新生代GC處理花費的時間。 -gc-gcnew-gcutil-gccause
FGC full GC發生的次數。 -gc -gccapacity -gcnew -gcnewcapacity -gcold -gcoldcapacity -gcpermcapacity -gcutil -gccause
FGCT full GC操作花費的時間。 -gc -gcold -gcoldcapacity -gcpermcapacity -gcutil -gccause
GCT GC操作花費的總時間。 -gc -gcold -gcoldcapacity -gcpermcapacity -gcutil -gccause
NGCMN 新生代最小空間容量,單位KB。 -gccapacity -gcnewcapacity
NGCMX 新生代最大空間容量,單位KB。 -gccapacity -gcnewcapacity
NGC 新生代當前空間容量,單位KB。 -gccapacity -gcnewcapacity
OGCMN 老年代最小空間容量,單位KB。 -gccapacity-gcoldcapacity
OGCMX 老年代最大空間容量,單位KB。 -gccapacity-gcoldcapacity
OGC 老年代當前空間容量制,單位KB。 -gccapacity -gcoldcapacity
PGCMN 持久代最小空間容量,單位KB。 -gccapacity -gcpermcapacity
PGCMX 持久代最大空間容量,單位KB。 -gccapacity -gcpermcapacity
PGC 持久代當前空間容量,單位KB。 -gccapacity -gcpermcapacity
PC 持久代當前空間大小,單位KB。 -gccapacity-gcpermcapacity
PU 持久代當前已用空間大小,單位KB。 -gc -gcold
LGCC 最後一次GC發生的原因。 -gccause
GCC 當前GC發生的原因。 -gccause
TT 老年化閾值。被移動到老年代之前,在新生代空存活的次數。 -gcnew
MTT 最大老年化閾值。被移動到老年代之前,在新生代空存活的次數。 -gcnew
DSS 倖存者區所需空間大小,單位KB。 -gcnew

jmap

生成堆儲存快照,使用方式:

jmap [ -option ] <pid>
選項 作用
dump 生成堆儲存快照,格式為:-dump:[live, ]format=b, file=<filename>,live說明是否只dump出存活的物件。
heap 顯示java堆詳細資訊,如使用那種回收器、引數配置、分代狀況等。
histo 顯示堆中物件統計資訊,包括類、例項數量、合計容量。

jstack

生成虛擬機器當前時刻的執行緒快照,幫助定位執行緒出現長時間停頓的原因,用法:

jstack <pid>

Monitor

Monitor是 Java中用以實現執行緒之間的互斥與協作的主要手段,它可以看成是物件或者Class的鎖。每一個物件都有,也僅有一個 monitor。下面這個圖,描述了執行緒和 Monitor之間關係,以及執行緒的狀態轉換圖:

進入區(Entrt Set):表示執行緒通過synchronized要求獲取物件的鎖,但並未得到。

擁有者(The Owner):表示執行緒成功競爭到物件鎖。

等待區(Wait Set):表示執行緒通過物件的wait方法,釋放物件的鎖,並在等待區等待被喚醒。

執行緒狀態

  • NEW,未啟動的。不會出現在Dump中。
  • RUNNABLE,在虛擬機器內執行的。
  • BLOCKED,等待獲得監視器鎖。
  • WATING,無限期等待另一個執行緒執行特定操作。
  • TIMED_WATING,有時限的等待另一個執行緒的特定操作。
  • TERMINATED,已退出的。

舉個例子:

package com.jiuyan.mountain.test;

import java.util.concurrent.TimeUnit;

/**
* Hello world!
* 
*/
public class App {

    public static void main(String[] args) throws InterruptedException {
        MyTask task = new MyTask();
        Thread t1 = new Thread(task);
        t1.setName("t1");
        Thread t2 = new Thread(task);
            t2.setName("t2");
            t1.start();
            t2.start();
   }

}

class MyTask implements Runnable {

    private Integer mutex;

    public MyTask() {
        mutex = 1;
    }

    @Override
    public void run() {
        synchronized (mutex) {
            while(true) {
                System.out.println(Thread.currentThread().getName());
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
             }
         }
    }

}

執行緒狀態:

"t2" prio=10 tid=0x00007f7b2013a800 nid=0x67fb waiting for monitor entry [0x00007f7b17087000]
java.lang.Thread.State: BLOCKED (on object monitor)
 at com.jiuyan.mountain.test.MyTask.run(App.java:35)
 - waiting to lock <0x00000007d6b6ddb8> (a java.lang.Integer)
 at java.lang.Thread.run(Thread.java:745)

"t1" prio=10 tid=0x00007f7b20139000 nid=0x67fa waiting on condition [0x00007f7b17188000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
 at java.lang.Thread.sleep(Native Method)

t1沒有搶到鎖,所以顯示BLOCKED。t2搶到了鎖,但是處於睡眠中,所以顯示TIMED_WAITING,有限等待某個條件來喚醒。
把睡眠的程式碼去掉,執行緒狀態變成了:

"t2" prio=10 tid=0x00007fa0a8102800 nid=0x6a15 waiting for monitor entry [0x00007fa09e37a000]
java.lang.Thread.State: BLOCKED (on object monitor)
 at com.jiuyan.mountain.test.MyTask.run(App.java:35)
 - waiting to lock <0x0000000784206650> (a java.lang.Integer)
 at java.lang.Thread.run(Thread.java:745)

"t1" prio=10 tid=0x00007fa0a8101000 nid=0x6a14 runnable [0x00007fa09e47b000]
java.lang.Thread.State: RUNNABLE
 at java.io.FileOutputStream.writeBytes(Native Method)

t1顯示RUNNABLE,說明正在執行,這裡需要額外說明一下,如果這個執行緒正在查詢資料庫,但是資料庫發生死鎖,雖然執行緒顯示在執行,實際上並沒有工作,對於IO型的執行緒別隻用執行緒狀態來判斷工作是否正常。
把MyTask的程式碼小改一下,執行緒拿到鎖之後執行wait,釋放鎖,進入等待區。

public void run() {
    synchronized (mutex) {
        if(mutex == 1) {
            try {
                mutex.wait();
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
     }
 }

執行緒狀態如下:

"t2" prio=10 tid=0x00007fc5a8112800 nid=0x5a58 in Object.wait() [0x00007fc59b58c000]
java.lang.Thread.State: WAITING (on object monitor)
 at java.lang.Object.wait(Native Method)

"t1" prio=10 tid=0x00007fc5a8111000 nid=0x5a57 in Object.wait() [0x00007fc59b68d000]
java.lang.Thread.State: WAITING (on object monitor)
 at java.lang.Object.wait(Native Method)

兩個執行緒都顯示WAITING,這次是無限期的,需要重新獲得鎖,所以後面跟了on object monitor

再來個死鎖的例子:

package com.jiuyan.mountain.test;

import java.util.concurrent.TimeUnit;

/**
* Hello world!
* 
*/
public class App {

     public static void main(String[] args) throws InterruptedException {
           MyTask task1 = new MyTask(true);
           MyTask task2 = new MyTask(false);
           Thread t1 = new Thread(task1);
           t1.setName("t1");
           Thread t2 = new Thread(task2);
           t2.setName("t2");
           t1.start();
           t2.start();
     }

}

class MyTask implements Runnable {

     private boolean flag;

     public MyTask(boolean flag) {
           this.flag = flag;
     }

     @Override
     public void run() {
           if(flag) {
                 synchronized (Mutex.mutex1) {
                       try {
                             TimeUnit.SECONDS.sleep(1);
                       } catch (InterruptedException e) {
                             // TODO Auto-generated catch block
                             e.printStackTrace();
                       }
                       synchronized (Mutex.mutex2) {
                             System.out.println("ok");
                       }
                 }
           } else {
                 synchronized (Mutex.mutex2) {
                       try {
                             TimeUnit.SECONDS.sleep(1);
                       } catch (InterruptedException e) {
                             // TODO Auto-generated catch block
                             e.printStackTrace();
                       }
                       synchronized (Mutex.mutex1) {
                             System.out.println("ok");
                       }
                 }
           }
     }

}

class Mutex {
    public static Integer mutex1 = 1;
    public static Integer mutex2 = 2;
}

執行緒狀態:

"t2" prio=10 tid=0x00007f5f9c122800 nid=0x3874 waiting for monitor entry [0x00007f5f67efd000]
java.lang.Thread.State: BLOCKED (on object monitor)
 at com.jiuyan.mountain.test.MyTask.run(App.java:55)
 - waiting to lock <0x00000007d6c45bd8> (a java.lang.Integer)
 - locked <0x00000007d6c45be8> (a java.lang.Integer)
 at java.lang.Thread.run(Thread.java:745)

"t1" prio=10 tid=0x00007f5f9c121000 nid=0x3873 waiting for monitor entry [0x00007f5f67ffe000]
java.lang.Thread.State: BLOCKED (on object monitor)
 at com.jiuyan.mountain.test.MyTask.run(App.java:43)
 - waiting to lock <0x00000007d6c45be8> (a java.lang.Integer)
 - locked <0x00000007d6c45bd8> (a java.lang.Integer)
 at java.lang.Thread.run(Thread.java:745)

Found one Java-level deadlock:
=============================
"t2":
waiting to lock monitor 0x00007f5f780062c8 (object 0x00000007d6c45bd8, a java.lang.Integer),
which is held by "t1"
"t1":
waiting to lock monitor 0x00007f5f78004ed8 (object 0x00000007d6c45be8, a java.lang.Integer),
which is held by "t2"

這個有點像哲學家就餐問題,每個執行緒都持有對方需要的鎖,那就執行不下去了。

調優策略

兩個基本原則:

  • 將轉移到老年代的物件數量降到最少。
  • 減少Full GC的執行時間。目標是Minor GC時間在100ms以內,Full GC時間在1s以內。

主要調優引數:

設定堆記憶體大小,這是最基本的。

  1. -Xms:啟動JVM時的堆記憶體空間。
  2. -Xmx:堆記憶體最大限制。

設定新生代大小。

新生代不宜太小,否則會有大量物件湧入老年代。

  1. -XX:NewRatio:新生代和老年代的佔比。
  2. -XX:NewSize:新生代空間。
  3. -XX:SurvivorRatio:伊甸園空間和倖存者空間的佔比。
  4. -XX:MaxTenuringThreshold:物件進入老年代的年齡閾值。

設定垃圾回收器

年輕代:-XX:+UseParNewGC。

老年代:-XX:+UseConcMarkSweepGC。

CMS可以將STW時間降到最低,但是不對記憶體進行壓縮,有可能出現“並行模式失敗”。比如老年代空間還有300MB空間,但是一些10MB的物件無法被順序的儲存。這時候會觸發壓縮處理,但是CMS GC模式下的壓縮處理時間要比Parallel GC長很多。

G1採用”標記-整理“演算法,解決了記憶體碎片問題,建立了可預測的停頓時間型別,能讓使用者指定在一個長度為M毫秒的時間段內,消耗在垃圾收集上的時間不得超過N毫秒。

相關文章