JAVA应用性能监控之基于JDK命令行工具监控

10次阅读

共计 9966 个字符,预计需要花费 25 分钟才能阅读完成。

一、JVM 参数类型

  1. JVM 参数类型
  • 标准参数
    -help
    -server -client
    -version -showversion
    -cp -classpath
    标准参数在 JVM 各个版本里基本不变,相对稳定。
  • X 参数
    非标准化参数,不同版本的 JVM 中有可能会变,但变化不大。
    -Xint:解释执行
    -Xcomp:第一次使用就编译成本地代码
    -Xmixed:混合模式,JVM 自己来决定是否编译成本地代码

  • XX 参数
    非标准化参数
    相对不稳定
    主要用于 JVM 调优和 Debug
    分类:

    Boolean 类型:格式:-XX:[+-]<name> 表示启动或者禁用 name 属性
      比如:-XX:+UseConcMarkSweepGC
           -XX:+UseG1GC
    非 Boolean 类型:格式:-XX:<name>=<value> 表示 name 属性的值是 value
      比如:-XX:MaxGCPauseMillis=500
           -XX:GCTimeRatio=19
      -Xmx -Xms:设置 JVM 最大内存和最小内存
           -Xmx 等价于 -XX:MaxHeapSize
           -Xms 等价于 -XX:InitialHeapSize
           通过 jinfo -flag MaxHeapSize <pid> 查看

    -xss 等价于 -XX:ThreadStackSize  线程栈大小

    默认开启一个线程,该线程的栈大小为 1024kb

jstat 查看 JVM 统计信息

options: -class, -compiler, -gc, -printcompilation 
  • 类装载

1000 10 代表每隔 1000ms 输出 10 次
  • 垃圾收集

-gc, -gcutil, -gccasue, -gcnew, -gcold

表示当前 JVM 内存每个分块的使用情况,C 代表总容量,U 代表使用量。-gc 输出结果:S0C、S1C、S0U、S1U:S0 和 S1 的总容量和使用量
    EC、EU:Eden 区总量与使用量
    OC、OU:Old 区总量和使用量
    MC、MU:Metaspace 区总量和使用量
    CCSC、CCSU:压缩类空间总量和使用量
    YGC、YGCT:YoungGC 的次数与次数
    FGC、FGCT:FullGC 的次数与时间
    GCT:总的 GC 时间
jstat -gc 23789 1000 10

JVM 内存结构:

非堆区为操作系统的本地内存,独立于 JVM 堆区之外,JDK7 叫 perm 区,JDK8 叫 Metaspace。CCS:当我们启用短指针时候,指向自己的对象,指向自己的 class 文件的短指针的时候,就会存在这个 CCS,不启用短指针时候,就不会存在这个 CCS。CodeCache:JVM 生成的 native code 存放的内存空间称之为 Code Cache;JIT 编译、JNI 等都会编译代码到 native code,其中 JIT 生成的 native code 占用了 Code Cache 的绝大部分空间。通过 jstat 可以查看 metaspace 相关指标,分别是 M(Metaspace - Percent Used),CCS(Compressed Class Space - Percent Used),MC(Metaspace Capacity - Current),MU(Metaspae Used),CCSC(Compressed Class Space Capacity - Current),CCSU(Compressed Class Space Used),MCMN(Metaspace Capacity - Minimum),MCMX(Metaspace Capacity - Maximum),CCSMN(Compressed Class Space Capacity - Minimum),CCSMX(Compressed Class Space Capacity - Maximum),其中最重要的是下面四个指标(MC & MU & CCSC & CCSU):- MC 表示 Klass Metaspace 以及 NoKlass Metaspace 两者总共 committed 的内存大小,单位是 KB,虽然从上面的定义里我们看到了是 capacity,但是实质上计算的时候并不是 capacity,而是 committed,这个是要注意的。- MU 这个无可厚非,说的就是 Klass Metaspace 以及 NoKlass Metaspace 两者已经使用了的内存大小。- CCSC 表示的是 Klass Metaspace 的已经被 commit 的内存大小,单位也是 KB
 - CCSU 表示 Klass Metaspace 的已经被使用的内存大小
  • JIT 编译
    -compiler、-printcompilation

二、jmap+MAT 实战内存溢出

模拟内存溢出

在 spring.io 中创建 springboot 项目,以 Maven 构建。
工程目录结构为:

pom.xml 内容为:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>zte.hdh</groupId>
    <artifactId>monitor_tuning</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>monitor_tuning</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <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>asm</groupId>
            <artifactId>asm</artifactId>
            <version>3.3.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

User 类:

package zte.hdh.monitor_tuning.zdh.hdh.monitor_tuning.chapter2;

public class User {
    private int id;
    private String name;

    public User(){}

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {return id;}

    public void setId(int id) {this.id = id;}

    public String getName() {return name;}

    public void setName(String name) {this.name = name;}
}

Metaspace 类

package zte.hdh.monitor_tuning.zdh.hdh.monitor_tuning.chapter2;

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

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

/**
 * https://blog.csdn.net/bolg_hero/article/details/78189621
 */
public class Metaspace extends ClassLoader {public static List<Class<?>> createClass() {
        // 类持有
        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 zte.hdh.monitor_tuning.zdh.hdh.monitor_tuning.chapter2;

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

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

@RestController
public class MemoryController {private List<User> userList = new ArrayList<>();
    private List<Class<?>> classList = new ArrayList<>();

    /**
     * 堆内存溢出
     * -Xmx32M -Xms32M
     * @return
     */
    @GetMapping("/heap")
    public String heap(){
        int i = 0;
        while(true){userList.add(new User(i++, UUID.randomUUID().toString()));
        }
    }

    /**
     * 非堆内存溢出
     * -XX:MetaspaceSize=32M -XX:MaxMetaspaceSize=32M
     * @return
     */
    @GetMapping("/nonheap")
    public String noheap(){
        int i = 0;
        while(true){classList.addAll(Metaspace.createClass());
        }
    }
}

MonitorTuningApplication 类:

package zte.hdh.monitor_tuning;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MonitorTuningApplication {public static void main(String[] args) {SpringApplication.run(MonitorTuningApplication.class, args);
    }

}
堆内存溢出,设置 JVM 参数 -Xmx32M -Xms32M,启动应用,访问 http://localhost:8080/heap,得到报错为:Exception in thread "http-nio-8080-exec-1" java.lang.OutOfMemoryError: GC overhead limit exceeded。非堆内存溢出,设置 JVM 参数 -XX:MetaspaceSize=32M -XX:MaxMetaspaceSize=32M,启用应用,访问 http://localhost:8080/nonheap,得到报错为:Exception in thread "http-nio-8080-exec-1" java.lang.OutOfMemoryError: Metaspace。

如何导出内存映像文件

如果是内存泄露,我们需要找到具体内存泄露的地方,哪里一直占有没有被释放。
Java 和 C ++ 中内存泄露不一样。在 C ++ 中内存泄露是指 new 了一个对象之后,结果把这个对象的指针丢了,这部分内存就永远得不到释放了。在 Java 中,new 了一个对象之后,占着内存,一直不释放。

导出内存映像文件方法:
  • 内存溢出自动导出:

    -XX:+HeapDumpOnOutOfMemoryError
    -XX:HeapDumpPath=./
  • 使用 jmap 命令手动导出:

    jmap -dump.format=b,file=heap.hprof 16940
        

MAT 分析内存溢出

Memory Analyzer (MAT),下载网址:http://www.eclipse.org/mat/
主要查看两个统计信息:

  • 对象数量

  • 对象占用内存大小

这两个统计信息都可以应用正则表达式来筛选出我们应用里面的对象,通过包名或者对象名来过滤。选中需要检查的对象后,右键选择 Merge Shortest Path to GC Roots -> exclude all phantom/weak/soft etc. references,得到对象的引用层次关系,如图所示:

这样很明显就发现内存泄露的地方。

三、jstack 实战死循环与死锁

jstack -options pid

JAVA 线程状态

线程状态

  • NEW: The thread has not yet started.
  • RUNNABLE: The thread is executing in the JVM.
  • BLOCKED: The thread is blocked waiting for a monitor lock.
  • WAITING: The thread is waiting indefinitely for another thread to perform a particular action.
  • TIMED_WAITING: The thread is waiting for another thread to perform an action for up to a specified waiting time.
  • TERMINATED: The thread has exited.

我们常见的 BLOCKED 状态,一般都是在请求锁,在请求资源之类。比如多线程操作数据库,一个耗时较多的操作,会导致哦其他对于库的写入操作受到影响。再比如操作系统等限制了可以打开的文件句柄数,如果系统里已经打开达到了阈值,但未进行正确的关闭,此时就会产生问题。
TIME_WAITING 一般是处于 sleep 方法、wait 方法和 join 等操作,正在等待时间。
RUNNABLE 则是我们喜欢的线程状态,在努力干活的线程。

值得关注的线程状态有:

  • 死锁,Deadlock(重点关注)
  • 执行中,Runnable
  • 等待资源,Waiting on condition(重点关注)
  • 等待获取监视器,Waiting on monitor entry(重点关注)
  • 暂停,Suspended
  • 对象等待中,Object.wait() 或 TIMED_WAITING
  • 阻塞,Blocked(重点关注)
  • 停止,Parked

Jstack

jstck 是什么?这个是 Oracle JDK 默认包含的一个用于打印执行 Java 进程的当前线程栈信息的工具。

jstack prints Java stack traces of Java threads for a given Java process or core file or a remote debug server. For each Java frame, the full class name, method name, ‘bci’ (byte code index) and line number, if available, are printed.

其中几个关键点:每一个 Java Frame 的全类名、方法名,如果能拿到行号的话,还会显示行号。使用 jstack 打印出来的信息,和一般应用遇到异常时的 printStackTrace 基本一样,只是那只是一个线程调用链的,这里通过工具 jstack,可以将应用内所有线程都打印出来。
用法:

命令名 < 可选参数 > + pid(进程 id)

实战死循环导致 CPU 飙高

CpuController 类:
`package zte.hdh.monitor_tuning.zdh.hdh.monitor_tuning.chapter2;

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

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

@RestController
public class CpuController {

@GetMapping("/loop")
public List<Long> loop(){String data = "{\"data\":[{\"partnerid\":]";
    return getPartneridsFromJson(data);
}

private 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<>(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){//do nothing}
        partners = partners.substring(commapos);
    }
    return list;
}

}
`
运行 jar,nohub java -jar monitor_tuning-0.0.1-SNAPSHOT.jar
浏览器访问 http://localhost:8080/loop
查看 CPU 负载,top
查看 jstack 线程信息,jstack 16108 > 16108.log
监控应用所有线程,top -p 16108 -H

printf “%x” 8247 => 2037
查询 jstack 日志发现多个线程在调用 CpuController.getPartneridsFromJson, 重点排查这个方法。

死锁实例

CpuController 类
`private Object lock1 = new Object();

private Object lock2 = new Object();
/**
 * 死锁
 * @return
 */
@RequestMapping("/deadlock")
public String deadlock(){new Thread(() -> {synchronized (lock1){
            try{Thread.sleep(1000);
            }catch (Exception e){e.printStackTrace();
            }
            synchronized (lock2){System.out.println("Thread1 over");
            }
        }
    }).start();
    new Thread(() -> {synchronized (lock2){
            try{Thread.sleep(1000);
            }catch (Exception e){e.printStackTrace();
            }
            synchronized (lock1){System.out.println("Thread2 over");
            }
        }
    }).start();
    return "deadLock";
}`

jstack 打印线程信息:

正文完
 0