深入理解JVM效能調優

曾燕輝發表於2018-07-02

轉載自:https://blog.csdn.net/elvis_lfc/article/details/52313400

我們知道,效能問題無非就這麼幾種:CPU、記憶體、磁碟IO、網路。那我們來逐一介紹以下相關的現象和一些可能出現的問題。

一、CPU過高。

檢視CPU最簡單的我們使用工作管理員檢視,如下圖所示,windows下使用工作管理員檢視,Linux下使用top檢視。

深入理解JVM效能調優
深入理解JVM效能調優

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

在top檢視下,對於多核的CPU,顯示的CPU資源有可能超過100%,因為這裡顯示的是所有CPU佔用百分百的總和,如果你需要看單個CPU的佔用情況,直接按鍵1就可以看到。如下圖所示,我的一臺測試機為8核16GB記憶體。

深入理解JVM效能調優

 在

top 檢視下,按鍵 shift+h 後,會顯示各個執行緒的 CPU 資源消耗情況,如下圖所示:
深入理解JVM效能調優

 我們也可以通過

sysstat 工具集的 pidstat 來檢視

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

安裝方法:

1、chmod +x configure

2、./configure

3、make

4、make install

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

深入理解JVM效能調優

 除了

top 、 pidstat 以外, vmstat 也可以進行取樣分析
深入理解JVM效能調優

 相關

top 、 pidstat 、 mstat 的用法大家可以去網上查詢。

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

CPU消耗過高主要分為使用者程式佔用CPU過高和核心程式佔用CPU過高(在Linux下top檢視下us指的是使用者程式,而sy是指核心程式),我們來看一個案例:

深入理解JVM效能調優

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

CPU 利用率,而紅色的線條表示核心使用率。部署 war 測試程式,執行如下圖所示:
深入理解JVM效能調優

 對於一個

web 程式,還沒有任何請求就佔用這麼多 CPU 資源,顯然是不正常的。並且我們看到,不是系統核心佔用的大量 CPU ,而是系統程式,那是哪一個程式的呢?我們來看一下。
深入理解JVM效能調優

 很明顯是我們的

java 程式,那是那個地方導致的呢?這就需要用到我們之前提到的效能監控工具。在此我們使用視覺化監控工具 VisualVM 。
深入理解JVM效能調優

 首先我們排除了是

GC 過於頻繁而導致大 CPU 過高,因為很明顯監控檢視上沒有 GC 的活動。然後我們開啟 profilter 去檢視以下,是那個執行緒導致了 CPU 的過高?
深入理解JVM效能調優

 前面一些執行緒都是容器使用的,而下面一個執行緒也一直在執行,那是什麼地方呼叫的呢?查詢程式碼中使用

ThredPoolExecutor 的地方。終於發現以下程式碼。

    private BlockingQueue queue;

    private Executor executor;

//……

publicvoid run() {

        while(true){

           try {

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

              if(null != sendMsg) {

                  sendForQueue(sendMsg);

              }

           catch (Exception e) {

              e.printStackTrace();

           }

       }

    }

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

深入理解JVM效能調優

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

null ,那這個語句就等價於下面的語句。

@Override

    publicvoid run() {

       while(true){

          

       }

    }

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

深入理解JVM效能調優

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

1 來看每個單獨 CPU 的情況!
深入理解JVM效能調優

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

CPU 被跑滿了。(因為一個單獨的死迴圈只能用到一顆 CPU ,都是單執行緒執行的)。

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

@Override

    publicvoid run() {

       while(true){

           try {

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

              sendForQueue(sendMsg);

           catch (Exception e) {

              e.printStackTrace();

           }

       }

    }

深入理解JVM效能調優

 再來監控

CPU 的變換,我們可以看到,基本上不消耗 CPU 資源(是我沒做任何的訪問哦,有使用者建立執行緒就會消耗)。
深入理解JVM效能調優

 再來看

java 程式的消耗,基本上不消耗 CPU 資源
深入理解JVM效能調優

 

深入理解JVM效能調優

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

深入理解JVM效能調優

 以上示例展示了

CPU 消耗過高情況下使用者執行緒佔用特別高的情況。也就是 Linux 下 top 檢視中 us 比較高的情況。發生這種情況的原因主要有以下幾種:程式不停的在執行無阻塞的迴圈、正則或者純粹的數學運算、 GC 特別頻繁。

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

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

 */

publicclass SY_Hign_TestCase {

   

    privatefinalstaticintLOCK_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();

    }

 

 

 

    abstractclass Task implements Runnable{

 

       protected Object lock;

 

       public Task(int index) {

           this.locklocks[index];

       }

       @Override

       publicvoid run() {

           while(true){  //迴圈執行自己要做的事情

              doSth();

           }

       }

       //做類自己要做的事情

       publicabstractvoid doSth();

    }

 

    //任務休眠自己的鎖

    class TaskA extends Task{

 

       public TaskA(int index) {

           super(index);

       }

 

       @Override

       publicvoid doSth() {

           synchronized (lock) {

              try {

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

              catch (InterruptedException e) {

                  e.printStackTrace();

              }

           }

       }

 

    }

 

    //任務喚醒所有鎖

    class TaskB extends Task{

      

       public TaskB(int index) {

           super(index);

        }

 

       @Override

       publicvoid doSth() {

           try {

              synchronized (lock) {

                  lock.notifyAll();

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

              }

           catch (InterruptedException e) {

              e.printStackTrace();

           }

       }

 

    }

    //啟動函式

    publicvoid start(){

       ExecutorService service = Executors.newCachedThreadPool();

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

           service.execute(new TaskA(i));

           service.execute(new TaskB(i));

       }

    }

    //主函式入口

    publicstaticvoid main(String[] args) {

       new SY_Hign_TestCase().start();

    }

 

}

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

深入理解JVM效能調優

 很明顯,

CPU 的核心佔用率很高,我們拿具體的資源監視器看一下:
深入理解JVM效能調優

 很明顯可以看出有很多執行緒切換佔用了大量的

CPU 資源。

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

深入理解JVM效能調優

 展開對應的

CPU 資源,我們可以清晰的看到如下情形:
深入理解JVM效能調優

 大家可以看到有大量的

sy 核心佔用,但是也有不少的 us , us 是因為我們啟用了大量的迴圈,而 sy 是因為大量執行緒切換導致的。

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

深入理解JVM效能調優

 二、檔案

IO 消耗過大,磁碟佇列高。

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

深入理解JVM效能調優

 這裡不但可以看到當前磁碟的負載資訊,佇列詳情,還能看到每個單獨的程式的資源消耗情況。

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

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

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

深入理解JVM效能調優
深入理解JVM效能調優

Iostat

深入理解JVM效能調優 

Iostat –x xvda 1 10做定時取樣

深入理解JVM效能調優

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

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

 */

publicclass IO_TestCase {

   

    private String fileNmae = "monitor.log";

   

    private String context ;

   

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

    privatefinalstaticintTHRED_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.contextnew String(sb);

       }

    }

    //寫檔案任務

    class Task implements Runnable{

 

       @Override

       publicvoid 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();

                  }

              }

           }

          

       }

    }

    //啟動函式

    publicvoid start(){

       ExecutorService service = Executors.newCachedThreadPool();

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

           service.execute(new Task());

    }

    //主函式入口

    publicstaticvoid main(String[] args) {

       new IO_TestCase().start();

    }

 

}

這段示例很簡單,通過建立一個和CPU個數相同的執行緒池,然後開啟這麼多執行緒一起讀寫同一個檔案,這樣就會因IO資源的競爭而導致IO的佇列很高,如下圖所示:

深入理解JVM效能調優

 關掉之後馬上就下來了

深入理解JVM效能調優

 我們把這個部署到

Linux 上觀看。
深入理解JVM效能調優

 這裡的

%idle 指的是系統沒有完成寫入的數量佔用 IO 總量的百分百,為什麼這麼高我們的系統還能承受?因為我這臺機器的記憶體為 16GB 的,我們來檢視以下 top 檢視就可以清晰的看到。
深入理解JVM效能調優

 佔用了大量的記憶體資源。

三、記憶體消耗

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

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

四、網路消耗過大

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

深入理解JVM效能調優

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

深入理解JVM效能調優

 Linux

平臺可以使用以下 sar 命令檢視

sar -n DEV 1 2

深入理解JVM效能調優

 欄位說明:

rxpck/s:每秒鐘接收的資料包

txpck/s:每秒鐘傳送的資料包

rxbyt/s:每秒鐘接收的位元組數

txbyt/s:每秒鐘傳送的位元組數

rxcmp/s:每秒鐘接收的壓縮資料包

txcmp/s:每秒鐘傳送的壓縮資料包

rxmcst/s:每秒鐘接收的多播資料包

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

五、程式執行緩慢

當CPU、記憶體、磁碟、網路都不高,程式還是執行緩慢的話,可能引發的原因大致有以下幾種:

1程式鎖競爭過於激烈,比如你只有2顆CPU,但是你啟用了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、  使用快取

相關文章