深入理解JVM性能調優

fmms 12年前發布 | 256K 次閱讀 JVM Java開發

    在上文中我們分析了很多性能監控工具,介紹這些工具的目的只有一個,那就是找出對應的性能瓶頸。盲目的性能調優是沒有效果的,只有充分知道了哪里出了問題,針對性的結果才是立竿見影的。解決了主要的性能問題,那些次要的性能問題也就不足為慮了!

我們知道,性能問題無非就這么幾種:CPU、內存、磁盤IO、網絡。那我們來逐一介紹以下相關的現象和一些可能出現的問題。

一、CPU過高。

查看CPU最簡單的我們使用任務管理器查看,如下圖所示,windows下使用任務管理器查看,Linux下使用top查看。

深入理解JVM性能調優

深入理解JVM性能調優
</span>

一般我們的服務器都采用Linux,因此我們重點關注一下Linux(注:windows模式下相信大家已經很熟悉了,并且前面我們已經提到,使用資源監視器可以很清楚的看到系統的各項參數,在這里我就不多做介紹了)

top視圖下,對于多核的CPU,顯示的CPU資源有可能超過100%,因為這里顯示的是所有CPU占用百分百的總和,如果你需要看單個CPU的占用情況,直接按鍵1就可以看到。如下圖所示,我的一臺測試機為816GB內存。

深入理解JVM性能調優

 在

</span>top</span> 視圖下,按鍵 shift+h</span> 后,會顯示各個線程的 CPU</span> 資源消耗情況,如下圖所示:

深入理解JVM性能調優

 我們也可以通過

</span>sysstat</span> 工具集的 pidstat</span> 來查看

注:sysstat下載地址:http://sebastien.godard.pagesperso-orange.fr/download.html

安裝方法:

1chmod +x configure

2./configure

3make

4make install

</div>

如輸入pidstat 1 2就會隔一秒在控制臺輸出一次當然CPU的情況,共輸出2

深入理解JVM性能調優

 除了

</span>top</span> pidstat</span> 以外, vmstat</span> 也可以進行采樣分析

深入理解JVM性能調優

 相關

</span>top</span> pidstat</span> mstat</span> 的用法大家可以去網上查找。

下面我們主要來介紹以下當出現CPU過高的時候,或者CPU不正常的時候,我們該如何去處理?

CPU消耗過高主要分為用戶進程占用CPU過高和內核進程占用CPU過高(在Linuxtop視圖下us指的是用戶進程,而sy是指內核進程),我們來看一個案例:

深入理解JVM性能調優

 程序運行前,系統運行平穩,其中藍色的線表示總的

</span>CPU</span> 利用率,而紅色的線條表示內核使用率。部署 war</span> 測試程序,運行如下圖所示:

深入理解JVM性能調優

 對于一個

</span>web</span> 程序,還沒有任何請求就占用這么多 CPU</span> 資源,顯然是不正常的。并且我們看到,不是系統內核占用的大量 CPU</span> ,而是系統進程,那是哪一個進程的呢?我們來看一下。

深入理解JVM性能調優

 很明顯是我們的

</span>java</span> 進程,那是那個地方導致的呢?這就需要用到我們之前提到的性能監控工具。在此我們使用可視化監控工具 VisualVM</span>

深入理解JVM性能調優

 首先我們排除了是

</span>GC</span> 過于頻繁而導致大 CPU</span> 過高,因為很明顯監控視圖上沒有 GC</span> 的活動。然后我們打開 profilter</span> 去查看以下,是那個線程導致了 CPU</span> 的過高?

深入理解JVM性能調優

 前面一些線程都是容器使用的,而下面一個線程也一直在執行,那是什么地方調用的呢?查找代碼中使用

</span>ThredPoolExecutor</span> 的地方。終于發現以下代碼。 </div>

    private BlockingQueue queue;

    private Executor executor;

//……

public void run() {

        while(true){

           try {

              SendMsg sendMsg = queue.poll();//從隊列中取出

              if(null != sendMsg) {

                  sendForQueue(sendMsg);

              }

           } catch (Exception e) {

              e.printStackTrace();

           }

       }

    }

</div>

問題很顯然了,我們看一下對應BlockingQueuepoll方法的API文檔。

深入理解JVM性能調優

 不難理解了,雖然使用了阻塞的隊列,但是使用了非阻塞的取法,當數據為空時直接返回

</span>null</span> ,那這個語句就等價于下面的語句。 </div>

@Override

    public void run() {

       while(true){

          

       }

    }

</div>

相當于死循環么,很顯然是非常耗費CPU資源的,并且我們還可以發現這樣的死循環是耗費的單顆CPU資源,因此可以解釋上圖為啥有一顆CPU占用特別高。我們來看一下部署在Linux下的top視圖。

深入理解JVM性能調優

 猛一看,不是很高么?我們按鍵

</span>1</span> 來看每個單獨 CPU</span> 的情況!

深入理解JVM性能調優

 這下看的很清楚了吧!明顯一顆

</span>CPU</span> 被跑滿了。(因為一個單獨的死循環只能用到一顆 CPU</span> ,都是單線程運行的)。

問題找到,馬上修復代碼為阻塞時存取,如下所示:

</div>

@Override

    public void run() {

       while(true){

           try {

              SendMsg sendMsg = queue.take();//從隊列中取出

              sendForQueue(sendMsg);

           } catch (Exception e) {

              e.printStackTrace();

           }

       }

    }

</div>

深入理解JVM性能調優

 再來監控

</span>CPU</span> 的變換,我們可以看到,基本上不消耗 CPU</span> 資源(是我沒做任何的訪問哦,有用戶建立線程就會消耗)。

深入理解JVM性能調優

 再來看

</span>java</span> 進程的消耗,基本上不消耗 CPU</span> 資源

深入理解JVM性能調優

 

深入理解JVM性能調優
</span>

再來看VisualVM的監控,我們就可以看到基本上都是容器的一些線程了

深入理解JVM性能調優

 以上示例展示了

</span>CPU</span> 消耗過高情況下用戶線程占用特別高的情況。也就是 Linux</span> top</span> 視圖中 us</span> 比較高的情況。發生這種情況的原因主要有以下幾種:程序不停的在執行無阻塞的循環、正則或者純粹的數學運算、 GC</span> 特別頻繁。

CPU過高還有一種情況是內核占用CPU很高。我們來看另外一個示例。

</div>

package com.yhj.jvm.monitor.cpu.sy;

 

import java.util.Random;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

 

/**

 * @Described:系統內核占用CPU過高測試用例

 * @author YHJ create at 2012-3-28 下午05:27:33

 * @FileNmae com.yhj.jvm.monitor.cpu.sy.SY_Hign_TestCase.java

 */

public class SY_Hign_TestCase {

   

    private final static int LOCK_COUNT = 1000;

 

    //默認初始化LOCK_COUNT個鎖對象

    private Object [] locks = new Object[LOCK_COUNT];

 

    private Random random = new Random();

 

    //構造時初始化對應的鎖對象

    public SY_Hign_TestCase() {

       for(int i=0;i<LOCK_COUNT;++i)

           locks[i]=new Object();

    }

 

 

 

    abstract class Task implements Runnable{

 

       protected Object lock;

 

       public Task(int index) {

           this.lock= locks[index];

       }

       @Override

       public void run() {

           while(true){  //循環執行自己要做的事情

              doSth();

           }

       }

       //做類自己要做的事情

       public abstract void doSth();

    }

 

    //任務A 休眠自己的鎖

    class TaskA extends Task{

 

       public TaskA(int index) {

           super(index);

       }

 

       @Override

       public void doSth() {

           synchronized (lock) {

              try {

                  lock.wait(random.nextInt(10));

              } catch (InterruptedException e) {

                  e.printStackTrace();

              }

           }

       }

 

    }

 

    //任務B 喚醒所有鎖

    class TaskB extends Task{

      

       public TaskB(int index) {

           super(index);

        }

 

       @Override

       public void doSth() {

           try {

              synchronized (lock) {

                  lock.notifyAll();

                  Thread.sleep(random.nextInt(10));

              }

           } catch (InterruptedException e) {

              e.printStackTrace();

           }

       }

 

    }

    //啟動函數

    public void start(){

       ExecutorService service = Executors.newCachedThreadPool();

       for(int i=0;i<LOCK_COUNT;++i){

           service.execute(new TaskA(i));

           service.execute(new TaskB(i));

       }

    }

    //主函數入口

    public static void main(String[] args) {

       new SY_Hign_TestCase().start();

    }

 

}

</div>

代碼很簡單,就是創建了2000個線程,讓一定的線程去等待,另外一個線程去釋放這些資源,這樣就會有大量的線程切換,我們來看下效果。

深入理解JVM性能調優

 很明顯,

</span>CPU</span> 的內核占用率很高,我們拿具體的資源監視器看一下:

深入理解JVM性能調優

 很明顯可以看出有很多線程切換占用了大量的

</span>CPU</span> 資源。

同樣的程序部署在Linux下,top視圖如下圖所示:

深入理解JVM性能調優

 展開對應的

</span>CPU</span> 資源,我們可以清晰的看到如下情形:

深入理解JVM性能調優

 大家可以看到有大量的

</span>sy</span> 內核占用,但是也有不少的 us</span> us</span> 是因為我們啟用了大量的循環,而 sy</span> 是因為大量線程切換導致的。

我們也可以使用vmstat來查看,如下圖所示:

深入理解JVM性能調優

 二、文件

</span>IO</span> 消耗過大,磁盤隊列高。

windows環境下,我們可以使用資源監視器查看對應的IO消耗,如下圖所示:

深入理解JVM性能調優

 這里不但可以看到當前磁盤的負載信息,隊列詳情,還能看到每個單獨的進程的資源消耗情況。

</span>

Linux下主要使用pidstatiostat等進行分析。如下圖所示

Pidstat –d –t –p [pid] {time} {count}

如:pidstat -d -t -p 18720 1 1

深入理解JVM性能調優

深入理解JVM性能調優
</span>

Iostat

深入理解JVM性能調優  

Iostat –x xvda 1 10做定時采樣

深入理解JVM性能調優

 廢話不多說,直接來示例,上干貨!

</span></div>

package com.yhj.jvm.monitor.io;

 

import java.io.BufferedWriter;

import java.io.FileWriter;

import java.io.IOException;

import java.util.concurrent.ExecutorService;

import java.util.concurrent.Executors;

 

/**

 * @DescribedIO測試用例

 * @author YHJ create at 2012-3-29 上午09:56:06

 * @FileNmae com.yhj.jvm.monitor.io.IO_TestCase.java

 */

public class IO_TestCase {

   

    private String fileNmae = "monitor.log";

   

    private String context ;

   

    // CPU處理器個數相同,既充分利用CPU資源,又導致線程頻繁切換

    private final static int THRED_COUNT = Runtime.getRuntime().availableProcessors();

   

    public IO_TestCase() {//加長寫文件的內容,拉長每次寫入的時間

       StringBuilder sb = new StringBuilder();

       for(int i=0;i<1000;++i){

           sb.append("context index :")

           .append(i)

           .append("\n");

           this.context= new String(sb);

       }

    }

    //寫文件任務

    class Task implements Runnable{

 

       @Override

       public void run() {

           while(true){

              BufferedWriter writer = null;

              try {

                  writer = new BufferedWriter(new FileWriter(fileNmae,true));//追加模式

                  writer.write(context);

              } catch (Exception e) {

                  e.printStackTrace();

              }finally{

                  try {

                     writer.close();

                  } catch (IOException e) {

                     e.printStackTrace();

                  }

              }

           }

          

       }

    }

    //啟動函數

    public void start(){

       ExecutorService service = Executors.newCachedThreadPool();

       for(int i=0;i<THRED_COUNT;++i)

           service.execute(new Task());

    }

    //主函數入口

    public static void main(String[] args) {

       new IO_TestCase().start();

    }

 

}

</div>

這段示例很簡單,通過創建一個和CPU個數相同的線程池,然后開啟這么多線程一起讀寫同一個文件,這樣就會因IO資源的競爭而導致IO的隊列很高,如下圖所示:

深入理解JVM性能調優

 關掉之后馬上就下來了

</span>

深入理解JVM性能調優

 我們把這個部署到

</span>Linux</span> 上觀看。

深入理解JVM性能調優

 這里的

</span>%idle</span> 指的是系統沒有完成寫入的數量占用 IO</span> 總量的百分百,為什么這么高我們的系統還能承受?因為我這臺機器的內存為 16GB</span> 的,我們來查看以下 top</span> 視圖就可以清晰的看到。

深入理解JVM性能調優

 占用了大量的內存資源。

</span>

三、內存消耗

對于JVM的內存模型大家已經很清楚了,前面我們講了JVM的性能監控工具。對于Java應用來說,出現問題主要消耗在于JVM的內存上,而JVM的內存,JDK已經給我們提供了很多的工具。在實際的生成環境,大部分應用會將-Xms-Xmx設置為相同的,避免運行期間不斷開辟內存。

對于內存消耗,還有一部分是直接物理內存的,不在堆空間,前面我們也寫過對應的示例。之前一個系統就是因為有大量的NIO操作,而NIO是使用物理內存的,并且開辟的物理內存是在觸發FULL GC的時候才進行回收的,但是當時的機器總內存為16GB 給堆的內存是14GB Edon1.5GB,也就是實際剩下給物理呢哦村的只有0.5GB,最終導致總是發生內存溢出,但監控堆、棧的內存消耗都不大。在這里我就不多寫了!

四、網絡消耗過大

Windows下使用本地網絡視圖可以監控當前的網絡流量大小

深入理解JVM性能調優

 更詳細的資料可以打開資源監視器,如下圖所示

</span>

深入理解JVM性能調優

 Linux

</span></span>平臺可以使用以下 sar</span> 命令查看

sar -n DEV 1 2

深入理解JVM性能調優

 字段說明:

</span>

rxpck/s:每秒鐘接收的數據包

txpck/s:每秒鐘發送的數據包

rxbyt/s:每秒鐘接收的字節數

txbyt/s:每秒鐘發送的字節數

rxcmp/s:每秒鐘接收的壓縮數據包

txcmp/s:每秒鐘發送的壓縮數據包

rxmcst/s:每秒鐘接收的多播數據包

Java程序一般不會出現網絡IO導致問題,因此在這里也不過的的闡述。

五、程序執行緩慢

CPU、內存、磁盤、網絡都不高,程序還是執行緩慢的話,可能引發的原因大致有以下幾種:

1程序鎖競爭過于激烈,比如你只有2CPU,但是你啟用了200個線程,就會導致大量的線程等待和切換,而這不會導致CPU很高,但是很多線程等待意味著你的程序運行很慢。

2未充分利用硬件資源。比如你的機器是16個核心的,但是你的程序是單線程運行的,即使你的程序優化的很好,當需要處理的資源比較多的時候,程序還會很慢,因此現在都在提倡分布式,通過大量廉價的PC機來提升程序的執行速度!

3其他服務器反應緩慢,如數據庫、緩存等。當大量做了分布式,程序CPU負載都很低,但是提交給數據庫的sql無法很快執行,也會特別慢。

總結一下,當出現性能問題的時候我們該怎么做?

一、CPU過高

1、  us過高

使用監控工具快讀定位哪里有死循環,大計算,對于死循環通過阻塞式隊列解決,對于大計算,建議分配單獨的機器做后臺計算,盡量不要影響用戶交互,如果一定要的話(如框計算、云計算),只能通過大量分布式來實現

2、  sy過高

最有效的方法就是減少進程,不是進程越多效率越高,一般來說線程數和CPU的核心數相同,這樣既不會造成線程切換,又不會浪費CPU資源

二、內存消耗過高

1、  及時釋放不必要的對象

2、  使用對象緩存池緩沖

3、  采用合理的緩存失效算法(還記得我們之前提到的弱引用、幽靈引用么?)

三、磁盤IO過高

1、  異步讀寫文件

2、  批量讀寫文件

3、  使用緩存技術

4、  采用合理的文件讀寫規則

四、網絡

1、增加寬帶流量

五、資源消耗不多但程序運行緩慢

1、使用并發包,減少鎖競爭

2、對于必須單線程執行的使用隊列處理

3、大量分布式處理

六、未充分利用硬件資源

1、  修改程序代碼,使用多線程處理

2、  修正外部資源瓶頸,做業務拆分

3、  使用緩存


轉自:http://yhjhappy234.blog.163.com/blog/static/3163283220122298232721/?suggestedreading&wumii

 本文由用戶 fmms 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!