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

一、JVM参数类型

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

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

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

    -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内容为:



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.1.7.RELEASE
         
    
    zte.hdh
    monitor_tuning
    0.0.1-SNAPSHOT
    monitor_tuning
    Demo project for Spring Boot

    
        1.8
    

    
        
            org.springframework.boot
            spring-boot-starter-web
        

        
            org.springframework.boot
            spring-boot-starter-test
            test
        

        
            asm
            asm
            3.3.1
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    

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> createClass() {
        // 类持有
        List> classes = new ArrayList>();
        // 循环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);
            // 定义构造函数方法
            MethodVisitor mw = cw.visitMethod(Opcodes.ACC_PUBLIC, "",
                    "()V", null, null);
            // 第一个指令为加载this
            mw.visitVarInsn(Opcodes.ALOAD, 0);
            // 第二个指令为调用父类Object的构造函数
            mw.visitMethodInsn(Opcodes.INVOKESPECIAL,"java/lang/Object",
                    "", "()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 userList = new ArrayList<>();
    private List> 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 loop(){
    String data = "{\"data\":[{\"partnerid\":]";
    return getPartneridsFromJson(data);
}

private static List getPartneridsFromJson(String data){
    //{\"data\":[{\"partnerid\":982,\"count\":\"10000\",\"cityid\":\"11\"},{\"partnerid\":983,\"count\":\"10000\",\"cityid\":\"11\"},{\"partnerid\":984,\"count\":\"10000\",\"cityid\":\"11\"}]}
    //上面是正常的数据
    List 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打印线程信息:

你可能感兴趣的