1. 程式人生 > >基於JDK命令行工具的監控

基於JDK命令行工具的監控

功能 https 名稱 get bin org listener spec pkgs

JVM的參數類型

JVM參數類型大體分為三種:

  • 標準參數,基本每個版本的JVM都有的參數,比較穩定不變
  • X參數,非標準化的參數,每個JVM版本的都有些不一樣,但是變化較小
  • XX參數,非標準化的參數,相對不穩定,每個JVM版本的變化都比較大,主要用於JVM調優和Debug

常見的標準參數:

  • -help
  • -server
  • -client
  • -version
  • -showversion
  • -cp
  • -classpath

常見的X參數:

  • -Xint : 解釋執行
  • -Xcomp : 第一次使用就編譯成本地代碼
  • -Xmixed : 混合模式,JVM自己來決定是否編譯成本地代碼,這是默認的模式

XX參數又分為兩大類,一種是Boolean類型,如下:

格式 :-XX : [ + - ] < name > 表示啟用或禁用name屬性
比如:
-XX:+UseConcMarkSweepGC 表示啟用UseConcMarkSweepGC
-XX:+UseG1GC 表示啟用UseG1GC

另一種則是key/value類型的,如下:

格式:-XX : < name > = < value > 表示name屬性的值是value
比如:
-XX:MaxGCPauseMillis=500 表示MaxGCPauseMillis屬性的值是500
-XX:GCTimeRatio=19 表示GCTimeRatio屬性的值是19

要說最常見的JVM參數應該是 -Xmx 與 -Xms 這兩個參數,前者用於指定初始化堆的大小,而後者用於指定堆的最大值。然後就是-Xss參數,它用於指定線程的堆棧大小。可以看到這三個參數都是以-X開頭的,它們是-X參數嗎?實際上不是的,它們是XX參數,是屬於一種縮寫形式:

-Xms 等價於 -XX:InitialHeapSize
-Xmx 等價於 -XX:MaxHeapSize
-Xss 等價於 -XX:ThreadStackSize


查看JVM運行時參數

查看JVM運行時的參數是很重要的,因為只有知道當前運行的參數值,才知道要如何去調優。我這裏的服務器跑了一個Tomcat,我們就以這個Tomcat進程來作為一個例子,該進程的pid是1200,如下:
技術分享圖片

常用的查看JVM運行時參數:

  • -XX:+PrintFlagsInitial 查看初始值
  • -XX:+PrintFlagsFinal 查看最終值
  • -XX:+UnlocakExperimentalVMOptions 解鎖實驗參數
  • -XX:+UnlocakDiagnosticVMOptions 解鎖診斷參數
  • -XX:+PrintCommandLineFlags 打印命令行參數

我們來看看-XX:+PrintFlagsInitial參數的使用方式,如下:

[root@server ~]# java -XX:+PrintFlagsFinal -version
     bool UseCodeCacheFlushing                      = true                                {product}
     bool UseCompiler                               = true                                {product}
     bool UseCompilerSafepoints                     = true                                {product}
     bool UseCompressedClassPointers               := true                                {lp64_product}
     bool UseCompressedOops                        := true                                {lp64_product}

加上-version是因為讓它最後的時候輸出版本信息,不然的話就會輸出幫助信息了。以上這裏只是截取了部分的內容,實際打印出來的內容是很多的,大約七百多行。可以看到截取的這部分的參數都是bool類型的(還有其他類型的),而且有 = 和 := 兩種符號,= 表示JVM的默認值, := 表示被用戶或JVM修改的值,也就是非默認值。

註:這種直接使用java命令 + 參數的方式,實際查看的是當前這條java命令的JVM運行時參數值。

我們來介紹一個命令:jps,這個命令與Linux的ps命令類似,也是查看進程的,但jps是專門查看Java進程的,使用也很簡單:

  • 功能描述: jps是用於查看有權訪問的hotspot虛擬機的進程. 當未指定hostid時,默認查看本機jvm進程,否者查看指定的hostid機器上的jvm進程,此時hostid所指機器必須開啟jstatd服務。 jps可以列出jvm進程lvmid,主類類名,main函數參數, jvm參數,jar名稱等信息。

以下簡單演示一下jps命令的常見使用方式:

[root@server ~]# jps  // 沒添加option的時候,默認列出進程編號和簡單的class或jar名稱
1200 Bootstrap
2847 Jps
[root@server ~]# jps -l  // 輸出應用程序主類完整package名稱或jar完整名稱.
2880 sun.tools.jps.Jps
1200 org.apache.catalina.startup.Bootstrap
[root@server ~]# jps -q  // 僅僅顯示進程編號,不顯示jar,class, main參數等信息.
1200
2901
[root@server ~]# jps -m // 輸出主函數傳入的參數.-m就是在執行程序時從命令行輸入的參數
1200 Bootstrap start
2911 Jps -m
[root@server ~]# jps -v // 列出jvm參數
1200 Bootstrap -Djava.util.logging.config.file=/home/tomcat/apache-tomcat-8.5.8/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dcatalina.base=/home/tomcat/apache-tomcat-8.5.8 -Dcatalina.home=/home/tomcat/apache-tomcat-8.5.8 -Djava.io.tmpdir=/home/tomcat/apache-tomcat-8.5.8/temp
2921 Jps -Dapplication.home=/usr/java/jdk1.8.0_111 -Xms8m
[root@server ~]#

還需了解更多的話,可以查看官方的文檔,jps命令的官方文檔地址如下:

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jps.html

如果我們需要查看一個運行時的Java進程的JVM參數,就可以使用jinfo命令。jinfo是jdk自帶的命令,可以用來查看正在運行的Java應用程序的擴展參數,甚至支持在運行時,修改部分參數。以下簡單演示一下jinfo命令的常見使用方式:

[root@server ~]# jinfo -flag MaxHeapSize 1200  // 查看該java進程的最大內存
-XX:MaxHeapSize=482344960
[root@server ~]# jinfo -flag UseConcMarkSweepGC 1200  // 查看是否使用了UseConcMarkSweepGC垃圾回收器
-XX:-UseConcMarkSweepGC
[root@server ~]# jinfo -flag UseG1GC 1200  // 查看是否使用了UseG1GC垃圾回收器
-XX:-UseG1GC
[root@server ~]# jinfo -flag UseParallelGC 1200  // 查看是否使用了UseParallelGC 1200垃圾回收器
-XX:-UseParallelGC
[root@server ~]# jinfo -flag PrintGC 1200  // 查看是否使用了PrintGC 
-XX:-PrintGC
[root@server ~]# jinfo -flag +PrintGC 1200  // 使用PrintGC,就只需要加上+號即可
[root@server ~]# jinfo -flag PrintGC 1200
-XX:+PrintGC
[root@server ~]# jinfo -flag -PrintGC 1200  // 不使用PrintGC ,就只需要加上-號即可
[root@server ~]# jinfo -flags 1200  // 查看手動賦過值的JVM參數
Attaching to process ID 1200, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.111-b14
Non-default VM flags: -XX:CICompilerCount=2 -XX:InitialHeapSize=31457280 -XX:MaxHeapSize=482344960 -XX:MaxNewSize=160759808 -XX:MinHeapDeltaBytes=196608 -XX:NewSize=10485760 -XX:OldSize=20971520 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops 
Command line:  -Djava.util.logging.config.file=/home/tomcat/apache-tomcat-8.5.8/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dcatalina.base=/home/tomcat/apache-tomcat-8.5.8 -Dcatalina.home=/home/tomcat/apache-tomcat-8.5.8 -Djava.io.tmpdir=/home/tomcat/apache-tomcat-8.5.8/temp
[root@server ~]# 

還需了解更多的話,可以查看官方的文檔,jinfo命令的官方文檔地址如下:

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jinfo.html#BCGEBFDD


jstat查看JVM統計信息

Jstat 用於監控基於HotSpot的JVM,對其堆的使用情況進行實時的命令行的統計,使用jstat我們可以對指定的JVM做如下監控:

  • 類的加載及卸載情況
  • 查看垃圾回收時的信息
  • 查看新生代、老生代及持久代的容量及使用情況
  • 查看新生代、老生代及持久代的垃圾收集情況,包括垃圾回收的次數及垃圾回收所占用的時間
  • 查看新生代中Eden區及Survior區中容量及分配情況等
  • 查看JIT編譯的信息

官方文檔地址如下:

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html#BEHHGFAE

查看類的加載及卸載情況的相關選項:

Option Displays
-class 類加載的行為統計

-class 類加載的行為統計,命令示例:

[root@server ~]# jstat -class 1200 1000 3
Loaded  Bytes  Unloaded  Bytes     Time   
  3249  6451.6        0     0.0       2.40
  3249  6451.6        0     0.0       2.40
  3249  6451.6        0     0.0       2.40
[root@server ~]#

命令說明:

  • -class 表示查看類的加載及卸載情況
  • 1200 指定進程的id
  • 1000 指定多少毫秒查看一次
  • 3 指定查看多少次,也就是輸出多少行信息

打印的信息說明:

  • Loaded 已加載的類的個數
  • Bytes 已加載的類所占用的空間大小
  • Unloaded 已卸載的類的個數
  • Bytes 已卸載的類所占用的空間大小
  • Time 執行類裝載和卸載操作所花費的時間

查看垃圾回收信息的相關選項:

Option Displays
-gc 垃圾回收堆的行為統計
-gcutil 垃圾回收統計概述(百分比)
-gccause 垃圾收集統計概述(同-gcutil)
-gcnew 新生代行為統計
-gcold 老年代和Metaspace區行為統計
-gccapacity 各個垃圾回收代容量(young,old,perm)和他們相應的空間統計
-gcnewcapacity 新生代與其相應的內存空間的統計
-gcoldcapacity 年老代行為統計
-gcmetacapacity Metaspace區大小統計

-gc 垃圾回收堆的行為統計,命令示例:

[root@server ~]# jstat -gc 1200
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
1024.0 1024.0  0.0    35.2   8192.0   4826.9   20480.0    17994.0   20864.0 20268.3 2432.0 2238.1     29    0.134   0      0.000    0.134
[root@server ~]# 

打印的信息說明,C即 Capacity 總容量,U即 Used 已使用的容量:

  • S0C : survivor0區的總容量
  • S1C : survivor1區的總容量
  • S0U : survivor0區已使用的容量
  • S1C : survivor1區已使用的容量
  • EC : Eden區的總容量
  • EU : Eden區已使用的容量
  • OC : Old區的總容量
  • OU : Old區已使用的容量
  • MC : 當前Metaspace區的總容量 (KB)
  • MU : Metaspace區的使用量 (KB)
  • CCSC : 壓縮類空間總量
  • CCSU : 壓縮類空間使用量
  • YGC : 新生代垃圾回收次數
  • YGCT : 新生代垃圾回收時間
  • FGC : 老年代垃圾回收次數
  • FGCT : 老年代垃圾回收時間
  • GCT : 垃圾回收總消耗時間

註:我這裏使用的是JDK1.8版本的,如果是其他版本的JDK在這一塊打印的信息會有些不一樣

JVM大致的內存結構圖(JDK1.8版本):
技術分享圖片

-gccapacity 各個垃圾回收代容量(young,old,perm)和他們相應的空間統計。(同-gc,還會輸出Java堆各區域使用到的最大、最小空間),命令示例:

[root@server ~]# jstat -gccapacity 1200
 NGCMN    NGCMX     NGC     S0C   S1C       EC      OGCMN      OGCMX       OGC         OC       MCMN     MCMX      MC     CCSMN    CCSMX     CCSC    YGC    FGC 
 10240.0 156992.0  10240.0 1024.0 1024.0   8192.0    20480.0   314048.0    20480.0    20480.0      0.0 1067008.0  20864.0      0.0 1048576.0   2432.0     29     0
[root@server ~]# 

打印的信息說明:

  • NGCMN : 新生代占用的最小空間
  • NGCMX : 新生代占用的最大空間
  • OGCMN : 老年代占用的最小空間
  • OGCMX : 老年代占用的最大空間
  • OGC:當前年老代的容量 (KB)
  • OC:當前年老代的空間 (KB)
  • MCMN : Metaspace占用的最小空間
  • MCMX : Metaspace占用的最大空間

-gcutil 垃圾回收統計概述(同-gc,輸出的是已使用空間占總空間的百分比)。命令示例:

[root@server ~]# jstat -gcutil 1200
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   
  0.00   3.44  90.53  87.86  97.14  92.03     29    0.134     0    0.000    0.134
[root@server ~]# 

-gccause 垃圾收集統計概述(垃圾收集統計概述(同-gcutil),附加最近兩次垃圾回收事件的原因)。命令示例:

[root@server ~]# jstat -gccause 1200
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT    LGCC                 GCC                 
  0.00   3.44  92.49  87.86  97.14  92.03     29    0.134     0    0.000    0.134 Allocation Failure   No GC               
[root@server ~]# 

打印的信息說明:

  • LGCC:最近垃圾回收的原因
  • GCC:當前垃圾回收的原因

-gcnew(統計新生代行為)。命令示例:

[root@server ~]# jstat -gcnew 1200
 S0C    S1C    S0U    S1U   TT MTT  DSS      EC       EU     YGC     YGCT  
1024.0 1024.0    0.0   35.2 15  15  512.0   8192.0   7738.1     29    0.134
[root@server ~]# 

打印的信息說明:

  • TT:Tenuring threshold(提升閾值)
  • MTT:最大的tenuring threshold
  • DSS:survivor區域大小 (KB)

-gcnewcapacity(新生代與其相應的內存空間的統計)。命令示例:

[root@server ~]# jstat -gcnewcapacity 1200
  NGCMN      NGCMX       NGC      S0CMX     S0C     S1CMX     S1C       ECMX        EC      YGC   FGC 
   10240.0   156992.0    10240.0  15680.0   1024.0  15680.0   1024.0   125632.0     8192.0    29     0
[root@server ~]# 

打印的信息說明:

  • NGC:當前年輕代的容量 (KB)
  • S0CMX:最大的S0空間 (KB)
  • S0C:當前S0空間 (KB)
  • ECMX:最大eden空間 (KB)
  • EC:當前eden空間 (KB)

-gcold(老年代和Metaspace區行為統計)。命令示例:

[root@server ~]# jstat -gcold 1200
   MC       MU      CCSC     CCSU       OC          OU       YGC    FGC    FGCT     GCT   
 20864.0  20268.3   2432.0   2238.1     20480.0     17994.0     29     0    0.000    0.134
[root@server ~]# 

-gcoldcapacity(老年代與其相應的內存空間的統計)。命令示例:

[root@server ~]# jstat -gcoldcapacity 1200
   OGCMN       OGCMX        OGC         OC       YGC   FGC    FGCT     GCT   
    20480.0    314048.0     20480.0     20480.0    29     0    0.000    0.134
[root@server ~]# 

-gcmetacapacity(Metaspace區與其相應內存空間的統計)。命令示例:

[root@server ~]# jstat -gcmetacapacity 1200
   MCMN       MCMX        MC       CCSMN      CCSMX       CCSC     YGC   FGC    FGCT     GCT   
       0.0  1067008.0    20864.0        0.0  1048576.0     2432.0    30     0    0.000    0.136
[root@server ~]# 

查看JIT編譯信息的相關選項:

Option Displays
-compiler HotSpt JIT編譯器行為統計
-printcompilation HotSpot編譯方法統計

-compiler HotSpt JIT編譯器行為統計,命令示例:

[root@server ~]# jstat -compiler 1200
Compiled Failed Invalid   Time   FailedType FailedMethod
    2332      1       0     4.80          1 org/apache/tomcat/util/IntrospectionUtils setProperty
[root@server ~]# 

打印的信息說明:

  • Compiled : 編譯數量
  • Failed : 編譯失敗數量
  • Invalid : 無效數量
  • Time : 編譯耗時
  • FailedType : 失敗類型
  • FailedMethod : 失敗方法的全限定名

-printcompilation HotSpot編譯方法統計,命令示例:

[root@server ~]# jstat -printcompilation 1200
Compiled  Size  Type Method
    2332      5    1 org/apache/tomcat/util/net/SocketWrapperBase getEndpoint
[root@server ~]# 

打印的信息說明:

  • Compiled:被執行的編譯任務的數量
  • Size:方法字節碼的字節數
  • Type:編譯類型
  • Method:編譯方法的類名和方法名。類名使用"/" 代替 "." 作為空間分隔符. 方法名是給出類的方法名. 格式是一致於HotSpot -XX:+PrintComplation 選項

演示堆區和非堆區的內存溢出

我們都知道部署在線上的項目,是不能夠直接修改其代碼或隨意關閉、重啟服務的,所以當發生內存溢出錯誤時,我們需要通過監控工具去分析錯誤的原因。所以本小節簡單演示一下JVM堆區和非堆區的內存溢出,然後我們再通過工具來分析內存溢出的原因。首先使用IDEA創建一個SpringBoot工程,工程的目錄結構如下:
技術分享圖片

我這裏只勾選了web和Lombok以及增加了asm依賴,因為在演示非堆區內存溢出時,我們需要通過asm來動態生成class文件。所以pom.xml文件裏所配置的依賴如下:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.16.22</version>
    </dependency>
    <dependency>
        <groupId>asm</groupId>
        <artifactId>asm</artifactId>
        <version>3.3.1</version>
    </dependency>
</dependencies>

先來演示堆區的內存溢出,為了能夠讓內存更快的溢出,所以我們需要設置JVM內存參數值。如下:
1、
技術分享圖片

2、
技術分享圖片

創建一個實體類,因為對象是存放在堆區的,所以我們需要有一個實體對象來制造內存的溢出。代碼如下:

package org.zero01.monitor_tuning.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private int id;
    private String name;
}

然後創建一個controller類,方便我們通過postman等工具去進行測試。代碼如下:

package org.zero01.monitor_tuning.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.zero01.monitor_tuning.vo.User;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * @program: monitor_tuning
 * @description: 演示內存溢出接口
 * @author: 01
 * @create: 2018-07-08 15:41
 **/
@RestController
public class MemoryController {

    // 對象的成員變量會隨著對象本身而存儲在堆上
    private List<User> userList = new ArrayList<>();

    /**
     * 演示堆區內存溢出接口
     * 設定jvm參數:-Xmx32M -Xms32M
     *
     * @return
     */
    @GetMapping("/heap")
    public String heap() {
        int i = 0;
        while (true) {
            // 所以不斷的往成員變量裏添加數據就會導致內存溢出
            userList.add(new User(i++, UUID.randomUUID().toString()));
        }
    }
}

啟動SpringBoot,訪問 localhost:8080/heap 後,控制臺輸出的錯誤日誌如下:
技術分享圖片

演示完堆區內存溢出後,我們再來看看非堆區的內存溢出,從之前的JVM內存結構圖可以看到,在JDK1.8中,非堆區就是Metaspace區。同樣的為了能夠讓內存更快的溢出,所以我們需要設置JVM的Metaspace區參數值如下:
技術分享圖片

Metaspace區可以存儲class,所以我們通過不斷的存儲class來制造Metaspace區的內存溢出。使用asm框架我們可以動態的創建class文件。新建一個 Metaspace 類,代碼如下:

package org.zero01.monitor_tuning.loader;

import java.util.ArrayList;
import java.util.List;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

/**
 * @program: monitor_tuning
 * @description: 繼承ClassLoader是為了方便調用defineClass方法,因為該方法的定義為protected
 * @author: 01
 * @create: 2018-07-08 15:58
 **/
public class Metaspace extends ClassLoader {

    /**
     * 動態創建class文件
     *
     * @return
     */
    public static List<Class<?>> createClasses() {
        // 類持有
        List<Class<?>> classes = new ArrayList<Class<?>>();
        // 循環1000w次生成1000w個不同的類。
        for (int i = 0; i < 10000000; ++i) {
            ClassWriter cw = new ClassWriter(0);
            // 定義一個類名稱為Class{i},它的訪問域為public,父類為java.lang.Object,不實現任何接口
            cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "Class" + i, null,
                    "java/lang/Object", null);
            // 定義構造函數<init>方法
            MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>",
                    "()V", null, null);
            // 第一個指令為加載this
            mw.visitVarInsn(Opcodes.ALOAD, 0);
            // 第二個指令為調用父類Object的構造函數
            mw.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object",
                    "<init>", "()V");
            // 第三條指令為return
            mw.visitInsn(Opcodes.RETURN);
            mw.visitMaxs(1, 1);
            mw.visitEnd();
            Metaspace test = new Metaspace();
            byte[] code = cw.toByteArray();
            // 定義類
            Class<?> exampleClass = test.defineClass("Class" + i, code, 0, code.length);
            classes.add(exampleClass);
        }
        return classes;
    }
}

在 MemoryController 類中增加一個成員變量和一個方法,用於制造非堆區的內存溢出。代碼如下:

package org.zero01.monitor_tuning.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.zero01.monitor_tuning.loader.Metaspace;
import org.zero01.monitor_tuning.vo.User;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

/**
 * @program: monitor_tuning
 * @description: 演示內存溢出接口
 * @author: 01
 * @create: 2018-07-08 15:41
 **/
@RestController
public class MemoryController {

    private List<User> userList = new ArrayList<>();
    // class會被放在Metaspace區
    private List<Class<?>> classList = new ArrayList<>();

    /**
     * 演示堆區內存溢出接口
     * 設定jvm參數:-Xmx32M -Xms32M
     *
     * @return
     */
    @GetMapping("/heap")
    public String heap() {
        int i = 0;
        while (true) {
            userList.add(new User(i++, UUID.randomUUID().toString()));
        }
    }

    /**
     * 演示非堆區內存溢出接口
     * 設定jvm參數:-XX:MetaspaceSize=32M -XX:MaxMetaspaceSize=32M
     * @return
     */
    @GetMapping("/nonheap")
    public String nonHeap() {
        int i = 0;
        while (true) {
            // 不斷的存儲class文件,就會導致Metaspace區內存溢出
            classList.addAll(Metaspace.createClasses());
        }
    }
}

啟動SpringBoot,訪問 localhost:8080/nonheap 後,控制臺輸出的錯誤日誌如下:
技術分享圖片


導出內存映像文件

上一小節中,我們演示了兩種內存溢出,堆區內存溢出與非堆區內存溢出。如果我們線上的項目出現這種內存溢出的錯誤該如何解決?我們一般主要通過分析內存映像文件,來查看是哪些類一直占用著內存沒有被釋放。

導出內存映像文件的幾種方式:

  • 第一種:當發生內存溢出時JVM自動導出,這種方式需要設置如下兩個JVM參數:
    -XX:+HeapDumpOnOutOfMemoryError
    -XX:HeapDumpPath=./
  • 第二種:使用jmap命令手動導出,我們一般都是使用這種方式,因為等到當發生內存溢出時再導出就晚了,我們應該盡量做到預防錯誤的發生

註:-XX:HeapDumpPath=./ 用於指定將內存映像文件導出到哪個路徑

我們先演示第一種導出內存映像文件的方式,同樣的,需要先設置一下JVM的參數,如下:
技術分享圖片

啟動SpringBoot,訪問 localhost:8080/heap 後,控制臺輸出的錯誤日誌如下,可以看到內存映像文件被導出到當前工程的根目錄了:
技術分享圖片

打開工程的根目錄,就可以看到這個內存映像文件:
技術分享圖片

接著我們再來演示一下使用jmap命令來導出內存映像文件,命令如下:

C:\Users\admin\Desktop>jps  // 查看進程的pid
10328 Jps
1100 Launcher
12124
1308 MonitorTuningApplication
C:\Users\admin\Desktop>jmap -dump:format=b,file=heap.hprof 1308  // 導出內存映像文件
Dumping heap to C:\Users\admin\Desktop\heap.hprof ...
Heap dump file created

C:\Users\admin\Desktop>

命令選項說明:

  • -dump 導出內存映像文件
  • format 指定文件為二進制格式
  • file 指定文件的名稱,默認導出到當前路徑

因為當前的路徑是在桌面,所以就導出到桌面上了:
技術分享圖片

如果需要了解更多關於jmap的用法,可以查閱官方文檔,地址如下:

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jmap.html#CEGCECJB


使用MAT工具分析內存溢出

在上一小節中,我們已經演示了兩種導出內存映像文件的方式。但是這些內存映像文件裏都是些什麽東西呢?我們要如何利用內存映像文件去分析問題所在呢?那這就需要用到另一個工具MAT了。

MAT是Eclipse的一個內存分析工具,全稱Memory Analyzer Tools,官網地址如下:

http://www.eclipse.org/mat/

MAT的下載地址如下:

http://www.eclipse.org/mat/downloads.php

下載並解壓之後,點擊MemoryAnalyzer.exe即可打開該工具,並不需要打開Eclipse,雖然下載的壓縮包裏包含了Eclipse:
技術分享圖片

正常打開後界面如下:
技術分享圖片

然後我們打開之前演示的發生內存溢出時,JVM自動導出的內存映像文件:
技術分享圖片
技術分享圖片
技術分享圖片

內存映像文件打開後,MAT會自動分析出一個餅狀圖,把可能出現問題的三個地方列了出來,並通過餅狀圖分為了三塊。Problem Suspect 1表示最有可能導致問題出現的原因所在,而且也可以看到,的確是指向了我們演示內存溢出的那個 MemoryController 類。上面也描述了,該類的一個實例所占用的內存達到了55.57%:
技術分享圖片

這樣我們就很輕易的找到了問題的所在,當然線上環境肯定不會這麽簡單。畢竟這是我們故意去制造的內存溢出,如果是實際的生產環境會更復雜一些。

所以我們還會進行更多的分析,例如查看所有類的實例對象的數量:
技術分享圖片

或者查看指定類的實例對象數量,可以看到,User這個類的實例對象有十萬多個,一個類的實例對象存在十萬多個,肯定是有問題的:
技術分享圖片

右鍵點擊這個有問題的對象,查看其強引用:
技術分享圖片

從下圖中,可以看到首先是Tomcat的一個TaskThread引用了MemoryController,而MemoryController裏包含了一個名為userList的集合類型成員變量,該集合中存放了十萬多個User實例對象,這下基本上就可以確定是這個MemoryController裏userList的問題了:
技術分享圖片

除此之外還可以查看對象所占的字節數,使用方式和查看對象數量是一樣的:
技術分享圖片

MAT的常用功能就先介紹到這裏,一般我們使用這些常用功能就已經能夠定位問題的所在了,而且這種圖形化的工具也比較好上手,這裏就不過多贅述了。


jstack與線程的狀態

jstack可以打印JVM內部所有的線程數據,是java虛擬機自帶的一種線程堆棧跟蹤工具。使用jstack打印線程堆棧信息時,可以將這些信息重定向到一個文件裏,這樣就相當於生成了JVM當前時刻的線程快照。線程快照是當前JVM內每一條線程正在執行的方法堆棧的集合,生成線程快照的主要目的是定位線程出現長時間停頓的原因,如線程間死鎖、死循環、請求外部資源導致的長時間等待等。 線程出現停頓的時候通過jstack來查看各個線程的調用堆棧,就可以知道沒有響應的線程到底在後臺做什麽事情,或者等待什麽資源。 如果java程序崩潰生成core文件,jstack工具可以用來獲得core文件的java stack和native stack的信息,從而可以輕松地知道java程序是如何崩潰和在程序何處發生問題。另外,jstack工具還可以附屬到正在運行的java程序中,看到當時運行的java程序的java stack和native stack的信息, 如果現在運行的java程序呈現hung的狀態,jstack是非常有用的。

jstack官方文檔地址如下:

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstack.html#BABGJDIF

使用jstack打印Java程序裏所有線程堆棧信息示例:

[root@server ~]# jps
1200 Bootstrap
4890 Jps
[root@server ~]# jstack 1200  // 直接加上pid即可打印該Java程序裏的所有線程堆棧信息
2018-07-08 21:48:01
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.111-b14 mixed mode):

"Attach Listener" #35 daemon prio=9 os_prio=0 tid=0x00007fd944006000 nid=0xb90 waiting on condition [0x0000000000000000]
   java.lang.Thread.State: RUNNABLE  // 該線程的狀態為RUNNABLE  

"http-nio-8080-exec-10" #34 daemon prio=5 os_prio=0 tid=0x00007fd96c31e800 nid=0x4d3 waiting on condition [0x00007fd9487ac000]
   java.lang.Thread.State: WAITING (parking)  // 該線程的狀態為WAITING 
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x00000000edae5bb8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
    at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
    at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:103)
    at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:31)
    at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:745)
    ...

註:nid是線程的唯一標識符,是16進制的,通常用於定位某一個線程

這裏只是截取了前面兩條線程的信息,可以看到這些線程都有一個java.lang.Thread.State參數,該參數的值就是該線程的狀態。

Java線程狀態:

  • NEW 未啟動的新線程
  • RUNNABLE 正在運行的線程
  • BLOCKED 阻塞狀態,一般都是在等待鎖資源
  • WAITING 等待狀態
  • TIMED_WAITING 有時間的等待狀態
  • TERMINATED 線程已退出

線程狀態轉換示意圖:
技術分享圖片


jstack實戰死循環與死鎖

本小節我們使用一個例子演示死循環與死鎖,然後介紹如何利用jstack分析、定位問題的所在。

在controller包中,新建一個 CpuController 類,用於演示發生死循環與死鎖時CPU占用率飆高的情況。這是一個解析json的代碼,並不需要註意代碼的細節,只需要知道訪問這個接口會導致死循環即可。代碼如下:

package org.zero01.monitor_tuning.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

/**
 * @program: monitor_tuning
 * @description: 演示死循環與死鎖
 * @author: 01
 * @create: 2018-07-08 22:14
 **/
@RestController
public class CpuController {

    /**
     * 演示死循環
     */
    @RequestMapping("/loop")
    public List<Long> loop() {
        String data = "{\"data\":[{\"partnerid\":]";
        return getPartneridsFromJson(data);
    }

    public static List<Long> getPartneridsFromJson(String data) {
        //{\"data\":[{\"partnerid\":982,\"count\":\"10000\",\"cityid\":\"11\"},{\"partnerid\":983,\"count\":\"10000\",\"cityid\":\"11\"},{\"partnerid\":984,\"count\":\"10000\",\"cityid\":\"11\"}]}
        //上面是正常的數據
        List<Long> list = new ArrayList<Long>(2);
        if (data == null || data.length() <= 0) {
            return list;
        }
        int datapos = data.indexOf("data");
        if (datapos < 0) {
            return list;
        }
        int leftBracket = data.indexOf("[", datapos);
        int rightBracket = data.indexOf("]", datapos);
        if (leftBracket < 0 || rightBracket < 0) {
            return list;
        }
        String partners = data.substring(leftBracket + 1, rightBracket);
        if (partners == null || partners.length() <= 0) {
            return list;
        }
        while (partners != null && partners.length() > 0) {
            int idpos = partners.indexOf("partnerid");
            if (idpos < 0) {
                break;
            }
            int colonpos = partners.indexOf(":", idpos);
            int commapos = partners.indexOf(",", idpos);
            if (colonpos < 0 || commapos < 0) {
                //partners = partners.substring(idpos+"partnerid".length());//1
                continue;
            }
            String pid = partners.substring(colonpos + 1, commapos);
            if (pid == null || pid.length() <= 0) {
                //partners = partners.substring(idpos+"partnerid".length());//2
                continue;
            }
            try {
                list.add(Long.parseLong(pid));
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            partners = partners.substring(commapos);
        }
        return list;
    }
}

將工程使用maven進行打包,並上傳到服務器中,打包命令如下:

mvn clean package -Dmaven.test.skip=true

將jar包上傳到服務器中,然後使用如下命令進行啟動:

[root@server ~]# nohup java -jar monitor_tuning-0.0.1-SNAPSHOT.jar &

接著使用瀏覽器開多幾個標簽頁來訪問該工程的接口,為了讓CPU負載更快飈上去:
技術分享圖片

在Linux的命令行輸入top命令來查看CPU負載情況,等那麽一兩分鐘後,會發現CPU的負載就上去了,如下:
技術分享圖片

當我們服務器的CPU像這樣負載很高的時候,就可以使用jstack命令去定位哪一個線程的CPU占用率最高。通過jstack命令打印線程的堆棧信息,並重定向到一個文件中:

[root@server ~]# jstack 4999 > loop.txt

接著使用top命令指定查看某個進程中的線程:

[root@server ~]# top -p 4999 -H

通過以上這個命令,可以看到該進程中占用率最高的那幾個線程,我們把占用率第一的線程的pid給記錄一下:
技術分享圖片

然後通過printf命令,將pid轉換成16進制的nid,實際上這裏的pid就是十進制的nid,如下:

[root@server ~]# printf "%x" 5016
1398
[root@server ~]#

得出nid後,使用vim命令打開loop.txt文件,通過nid來搜索該線程的數據:
技術分享圖片

如上,通過分析線程堆棧的信息,就能定位到是哪個類的哪個方法裏的哪句代碼出了問題,這就是如何利用jstack命令,定位問題代碼。


以上演示完如何定位發生死循環的代碼後,接下來就是演示一下如何使用jstack定位發生死鎖的代碼。首先,在CpuController類中,增加如下代碼:

private Object lock1 = new Object();
private Object lock2 = new Object();

/**
 * 演示死鎖
 * */
@RequestMapping("/deadlock")
public String deadlock(){
    new Thread(()->{
        synchronized(lock1) {
            try {Thread.sleep(1000);}catch(Exception e) {}
            synchronized(lock2) {
                System.out.println("Thread1 over");
            }
        }
    }) .start();
    new Thread(()->{
        synchronized(lock2) {
            try {Thread.sleep(1000);}catch(Exception e) {}
            synchronized(lock1) {
                System.out.println("Thread2 over");
            }
        }
    }) .start();
    return "deadlock";
}

增加完以上代碼後,重新使用maven命令進行打包。

回到服務器上,殺掉之前啟動的服務,並把舊的jar包給刪除掉:

[root@server ~]# jps
4999 jar
5103 Jps
[root@server ~]# kill -9 4999  // 殺掉進程
[root@server ~]# rm -rf monitor_tuning-0.0.1-SNAPSHOT.jar  // 刪除之前的jar包

刪除掉舊的jar包後,再重新上傳新打包好的jar包,然後和之前一樣使用如下命令運行該jar包:

[root@server ~]# nohup java -jar monitor_tuning-0.0.1-SNAPSHOT.jar &

成功運行後,同樣的使用瀏覽器進行訪問,可以看到是能夠正常返回數據的,這是因為發生死鎖的是子線程,並不會影響主線程:
技術分享圖片

那麽我們要怎麽定位死鎖發生的代碼呢?因為這種情況下的死鎖和死循環不一樣,並不會導致CPU負載率的飆高。所以我們無法使用之前那種方式去定位問題代碼,但jstack比較好的一點就是,會自動幫我們找出死鎖。和之前一樣,使用如下命令生成一個線程快照文件:

[root@server ~]# jps
5128 jar
5177 Jps
[root@server ~]# jstack 5128 > deadlock.txt
[root@server ~]# vim deadlock.txt

使用vim打開該文件後,直接定位到文件的末尾,就可以看到死鎖的信息,jstack會自動找出死鎖,並把死鎖信息放在末尾。我已經使用藍色和紅色框框標出了兩個線程互相等待的鎖:
技術分享圖片

基於JDK命令行工具的監控