基于JDK命令行工具的监控

206 阅读25分钟
原文链接: blog.51cto.com

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,如下:
基于JDK命令行工具的监控

常用的查看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命令的官方文档地址如下:

docs.oracle.com/javase/8/do…

如果我们需要查看一个运行时的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命令的官方文档地址如下:

docs.oracle.com/javase/8/do…


jstat查看JVM统计信息

Jstat 用于监控基于HotSpot的JVM,对其堆的使用情况进行实时的命令行的统计,使用jstat我们可以对指定的JVM做如下监控:

  • 类的加载及卸载情况
  • 查看垃圾回收时的信息
  • 查看新生代、老生代及持久代的容量及使用情况
  • 查看新生代、老生代及持久代的垃圾收集情况,包括垃圾回收的次数及垃圾回收所占用的时间
  • 查看新生代中Eden区及Survior区中容量及分配情况等
  • 查看JIT编译的信息

官方文档地址如下:

docs.oracle.com/javase/8/do…

查看类的加载及卸载情况的相关选项:

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版本):
基于JDK命令行工具的监控

-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工程,工程的目录结构如下:
基于JDK命令行工具的监控

我这里只勾选了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、
基于JDK命令行工具的监控

2、
基于JDK命令行工具的监控

创建一个实体类,因为对象是存放在堆区的,所以我们需要有一个实体对象来制造内存的溢出。代码如下:

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 后,控制台输出的错误日志如下:
基于JDK命令行工具的监控

演示完堆区内存溢出后,我们再来看看非堆区的内存溢出,从之前的JVM内存结构图可以看到,在JDK1.8中,非堆区就是Metaspace区。同样的为了能够让内存更快的溢出,所以我们需要设置JVM的Metaspace区参数值如下:
基于JDK命令行工具的监控

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 后,控制台输出的错误日志如下:
基于JDK命令行工具的监控


导出内存映像文件

上一小节中,我们演示了两种内存溢出,堆区内存溢出与非堆区内存溢出。如果我们线上的项目出现这种内存溢出的错误该如何解决?我们一般主要通过分析内存映像文件,来查看是哪些类一直占用着内存没有被释放。

导出内存映像文件的几种方式:

  • 第一种:当发生内存溢出时JVM自动导出,这种方式需要设置如下两个JVM参数:
    -XX:+HeapDumpOnOutOfMemoryError
    -XX:HeapDumpPath=./
  • 第二种:使用jmap命令手动导出,我们一般都是使用这种方式,因为等到当发生内存溢出时再导出就晚了,我们应该尽量做到预防错误的发生

注:-XX:HeapDumpPath=./ 用于指定将内存映像文件导出到哪个路径

我们先演示第一种导出内存映像文件的方式,同样的,需要先设置一下JVM的参数,如下:
基于JDK命令行工具的监控

启动SpringBoot,访问 localhost:8080/heap 后,控制台输出的错误日志如下,可以看到内存映像文件被导出到当前工程的根目录了:
基于JDK命令行工具的监控

打开工程的根目录,就可以看到这个内存映像文件:
基于JDK命令行工具的监控

接着我们再来演示一下使用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 指定文件的名称,默认导出到当前路径

因为当前的路径是在桌面,所以就导出到桌面上了:
基于JDK命令行工具的监控

如果需要了解更多关于jmap的用法,可以查阅官方文档,地址如下:

docs.oracle.com/javase/8/do…


使用MAT工具分析内存溢出

在上一小节中,我们已经演示了两种导出内存映像文件的方式。但是这些内存映像文件里都是些什么东西呢?我们要如何利用内存映像文件去分析问题所在呢?那这就需要用到另一个工具MAT了。

MAT是Eclipse的一个内存分析工具,全称Memory Analyzer Tools,官网地址如下:

www.eclipse.org/mat/

MAT的下载地址如下:

www.eclipse.org/mat/downloa…

下载并解压之后,点击MemoryAnalyzer.exe即可打开该工具,并不需要打开Eclipse,虽然下载的压缩包里包含了Eclipse:
基于JDK命令行工具的监控

正常打开后界面如下:
基于JDK命令行工具的监控

然后我们打开之前演示的发生内存溢出时,JVM自动导出的内存映像文件:
基于JDK命令行工具的监控
基于JDK命令行工具的监控
基于JDK命令行工具的监控

内存映像文件打开后,MAT会自动分析出一个饼状图,把可能出现问题的三个地方列了出来,并通过饼状图分为了三块。Problem Suspect 1表示最有可能导致问题出现的原因所在,而且也可以看到,的确是指向了我们演示内存溢出的那个 MemoryController 类。上面也描述了,该类的一个实例所占用的内存达到了55.57%:
基于JDK命令行工具的监控

这样我们就很轻易的找到了问题的所在,当然线上环境肯定不会这么简单。毕竟这是我们故意去制造的内存溢出,如果是实际的生产环境会更复杂一些。

所以我们还会进行更多的分析,例如查看所有类的实例对象的数量:
基于JDK命令行工具的监控

或者查看指定类的实例对象数量,可以看到,User这个类的实例对象有十万多个,一个类的实例对象存在十万多个,肯定是有问题的:
基于JDK命令行工具的监控

右键点击这个有问题的对象,查看其强引用:
基于JDK命令行工具的监控

从下图中,可以看到首先是Tomcat的一个TaskThread引用了MemoryController,而MemoryController里包含了一个名为userList的集合类型成员变量,该集合中存放了十万多个User实例对象,这下基本上就可以确定是这个MemoryController里userList的问题了:
基于JDK命令行工具的监控

除此之外还可以查看对象所占的字节数,使用方式和查看对象数量是一样的:
基于JDK命令行工具的监控

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官方文档地址如下:

docs.oracle.com/javase/8/do…

使用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 线程已退出

线程状态转换示意图:
基于JDK命令行工具的监控


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负载更快飚上去:
基于JDK命令行工具的监控

在Linux的命令行输入top命令来查看CPU负载情况,等那么一两分钟后,会发现CPU的负载就上去了,如下:
基于JDK命令行工具的监控

当我们服务器的CPU像这样负载很高的时候,就可以使用jstack命令去定位哪一个线程的CPU占用率最高。通过jstack命令打印线程的堆栈信息,并重定向到一个文件中:

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

接着使用top命令指定查看某个进程中的线程:

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

通过以上这个命令,可以看到该进程中占用率最高的那几个线程,我们把占用率第一的线程的pid给记录一下:
基于JDK命令行工具的监控

然后通过printf命令,将pid转换成16进制的nid,实际上这里的pid就是十进制的nid,如下:

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

得出nid后,使用vim命令打开loop.txt文件,通过nid来搜索该线程的数据:
基于JDK命令行工具的监控

如上,通过分析线程堆栈的信息,就能定位到是哪个类的哪个方法里的哪句代码出了问题,这就是如何利用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 &

成功运行后,同样的使用浏览器进行访问,可以看到是能够正常返回数据的,这是因为发生死锁的是子线程,并不会影响主线程:
基于JDK命令行工具的监控

那么我们要怎么定位死锁发生的代码呢?因为这种情况下的死锁和死循环不一样,并不会导致CPU负载率的飙高。所以我们无法使用之前那种方式去定位问题代码,但jstack比较好的一点就是,会自动帮我们找出死锁。和之前一样,使用如下命令生成一个线程快照文件:

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

使用vim打开该文件后,直接定位到文件的末尾,就可以看到死锁的信息,jstack会自动找出死锁,并把死锁信息放在末尾。我已经使用蓝色和红色框框标出了两个线程互相等待的锁:
基于JDK命令行工具的监控