【JVM专题】JVM调优工具详解及调优实战

前言

在JavaWeb应用开发、运营维护过程中,我们难免会遇到一些问题,如:

  • OOM,OutOfMemoryError
  • 线程死锁
  • Java进程CPU使用率爆表

等等问题。很多人遇到上述问题通常都是重启解决,然后事后再分析原因。但是我们都知道,这样处理甚至连治标都算不上,有时候重启也不一定有效。
其实市面上也有很多可视化分析工具,但是从本质上来说,他们往往都是基于JDK自带的工具来实现的。他们就是:jps,jmap,jstack,jinfo,jstat命令

前置知识

  • 你需要对JVM内存模型有一定了解
  • 需要了解年轻代、老年代的对象特点,以及对象进入老年代的细则

课程内容

jps

  • 介绍:jps是用来获取机器运行的java进程id的,也是后续命令的基础
  • 常用命令:输入jps -lv可显示系统正在运行的java程序的pid,以及名称和显示指定的jvm参数,如下:
jps -lv
25234 server-1.0.0.jar -Xms4096m -Xmx4096m -Xmn2048m -Dspring.profiles.active=test
32398 sun.tools.jps.Jps -Denv.class.path=.::/root/.data/jdk1.8/lib:/root/.data/jdk1.8/jre/lib -Dapplication.home=/root/.data/jdk1.8 -Xms8m

jmap

  • 介绍:jmap,用来获取进程的内存情况,实例个数以及占用内存大小
  • 常用命令:
    • jmap -heap [进程ID],用来显示堆信息
    • jmap -histo [进程id],按类型显示实例个数及占用空间,可用于排查异常情况,如内存泄露引发的大量对象占用内存不释放

下面演示一下这两个命令的效果:

jmap -heap 25234
Attaching to process ID 25234, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.161-b12

using thread-local object allocation.
Parallel GC with 8 thread(s)

# 堆配置信息
Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 4294967296 (4096.0MB)
   NewSize                  = 2147483648 (2048.0MB)
   MaxNewSize               = 2147483648 (2048.0MB)
   OldSize                  = 2147483648 (2048.0MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

# 堆使用情况
Heap Usage:
# 年轻代
PS Young Generation
Eden Space:
   capacity = 2100822016 (2003.5MB)
   used     = 1513474088 (1443.3613662719727MB)
   free     = 587347928 (560.1386337280273MB)
   72.04199482265898% used
From Space:
   capacity = 23592960 (22.5MB)
   used     = 6611392 (6.30511474609375MB)
   free     = 16981568 (16.19488525390625MB)
   28.02273220486111% used
To Space:
   capacity = 23068672 (22.0MB)
   used     = 0 (0.0MB)
   free     = 23068672 (22.0MB)
   0.0% used
# 老年代
PS Old Generation
   capacity = 2147483648 (2048.0MB)
   used     = 1354490616 (1291.7429122924805MB)
   free     = 792993032 (756.2570877075195MB)
   63.07338438928127% used

再来看看另一个命令jmap -histo

jmap -histo 25234

在这里插入图片描述
上面输出的内容,各字段的含义如下:

  • num:序号
  • instances:实例数量
  • bytes:占用空间大小
  • class name:类名称。[C is a char[],[S is a short[],[I is a int[],[B is a byte[],[[I is a int[][]

jstack

  • 介绍:主要用于查看Java进程中线程的状态信息、堆栈信息、锁信息等,它可以在Java应用程序运行时对线程状态进行快照,输出每个线程正在调用的方法列表、状态信息以及线程间的同步等待情况。通过jstack可以帮助开发人员定位线程死锁、死循环(死循环其实就是cpu占用率高的代码)、内存泄漏等问题。具体功能包括:
    • 显示Java虚拟机内所有线程的快照信息
    • 显示某个线程的堆栈信息
    • 显示某个线程或整个Java进程内锁的持有情况
    • 检查某个Java进程内的堆信息
    • 显示某个Java进程内的JNI信息
  • 常用命令:jstack [进程id],用于显示堆栈信息,通常用于查看是否存在死锁
    比如下列死锁代码演示
public class DeadLockTest {

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

   public static void main(String[] args) {
      new Thread(() -> {
         synchronized (lock1) {
            try {
               System.out.println("thread1 begin");
               Thread.sleep(5000);
            } catch (InterruptedException e) {
            }
            synchronized (lock2) {
               System.out.println("thread1 end");
            }
         }
      }).start();

      new Thread(() -> {
         synchronized (lock2) {
            try {
               System.out.println("thread2 begin");
               Thread.sleep(5000);
            } catch (InterruptedException e) {
            }
            synchronized (lock1) {
               System.out.println("thread2 end");
            }
         }
      }).start();

      System.out.println("main thread end");
   }
}

运行后,我们输入jstack + 进程id后输出如下:
在这里插入图片描述
看,我们甚至可以从上图中看到线程持有锁的信息,以及代码调用行。上面输出的内容,各字段的含义如下:

  • Thread-1:线程名
  • prio=5:优先级=5
  • tid=0x000000001fa9e000:threadid
  • nid=0x2d64:线程对应的本地线程标识nid,native id
  • java.lang.Thread.State: BLOCKED:线程状态

通过上面这个命令,我们还能找出占用CPU最高的线程堆栈信息,示例代码及linux操作步骤如下:

package com.tuling.jvm;

/**
 * 运行此代码,cpu会飙高
 */
public class Math {

    public static final int initData = 666;
    public static User user = new User();

    public int compute() {  //一个方法对应一块栈帧内存区域
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }

    public static void main(String[] args) {
        Math math = new Math();
        while (true){
            math.compute();
        }
    }
}
  1. 使用命令top -p <pid> ,显示你的java进程的内存情况,pid是你的java进程号,比如:19663
    在这里插入图片描述

  2. 按H,获取每个线程的内存使用情况
    在这里插入图片描述

  3. 找到内存和cpu占用最高的线程tid,比如:19664

  4. 转为十六进制得到 0x4cd0,此为线程id的十六进制表示

  5. 执行 jstack 19663|grep -A 10 4cd0,得到线程堆栈信息中 4cd0 这个线程所在行的后面10行,从堆栈中可以发现导致cpu飙高的调用方法
    在这里插入图片描述

  6. 查看对应的堆栈信息找出可能存在问题的代码

jinfo

  • 介绍:查看正在运行的Java应用程序的扩展参数 ,非显示指定的配置参数等
  • 常用命令:jinfo -flags [进程id]

演示如下:

jinfo -flags 25234
Attaching to process ID 25234, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.161-b12
Non-default VM flags: -XX:CICompilerCount=4 -XX:InitialHeapSize=4294967296 -XX:MaxHeapSize=4294967296 -XX:MaxNewSize=2147483648 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=2147483648 -XX:OldSize=2147483648 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseFastUnorderedTimeStamps -XX:+UseParallelGC
Command line:  -Xms4096m -Xmx4096m -Xmn2048m -Dspring.profiles.active=prod

上面显示了两种类型:

  • Non-default VM flags:非默认的JVM标识
  • Command line:我们自己设置的JVM参数

其实,jinfo的功能不止于此,它还支持显示、动态启用/禁用、指定名称的参数配置,甚至动态修改某些JVM参数,之所以是某些,是因为不是所有的JVM参数都可以修改的,这里就不作演示了。详情可以看《Java的jinfo命令使用详解》这个大哥的文章。
命令如下:

jinfo -flag PrintGC 25234	// 查看进程25234的PrintGC参数值
jinfo -flag +PrintGC 25234	// 开启进程25234简单GC日志模式
jinfo -flag -PrintGC 25234	// 禁用进程25234简单GC日志模式
jinfo -flag MinHeapFreeRatio=30 25234 // 不需要重启Java虚拟机,修改MinHeapFreeRatio的值

// 我们可以通过下面这个命令来显示什么参数支持动态修改
java -XX:+PrintFlagsInitial|grep manageable

jstat

  • 介绍:jstat命令可以查看堆内存各部分的使用量,以及加载类的数量
  • 常用命令:jstat -gc [进程id] [间隔时间,ms] [查询次数],用于垃圾回收统计
jstat -gc 25234 1000 10
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
21504.0 22528.0  0.0   8784.1 2051584.0 82132.2  2097152.0  1322928.7  213592.0 192352.5 25728.0 21515.7    538   17.258   7      2.904   20.162
21504.0 22528.0  0.0   8784.1 2051584.0 82132.2  2097152.0  1322928.7  213592.0 192352.5 25728.0 21515.7    538   17.258   7      2.904   20.162
21504.0 22528.0  0.0   8784.1 2051584.0 82132.2  2097152.0  1322928.7  213592.0 192352.5 25728.0 21515.7    538   17.258   7      2.904   20.162
21504.0 22528.0  0.0   8784.1 2051584.0 82132.2  2097152.0  1322928.7  213592.0 192352.5 25728.0 21515.7    538   17.258   7      2.904   20.162
21504.0 22528.0  0.0   8784.1 2051584.0 82632.6  2097152.0  1322928.7  213592.0 192352.5 25728.0 21515.7    538   17.258   7      2.904   20.162
21504.0 22528.0  0.0   8784.1 2051584.0 82632.6  2097152.0  1322928.7  213592.0 192352.5 25728.0 21515.7    538   17.258   7      2.904   20.162
21504.0 22528.0  0.0   8784.1 2051584.0 82632.6  2097152.0  1322928.7  213592.0 192352.5 25728.0 21515.7    538   17.258   7      2.904   20.162
21504.0 22528.0  0.0   8784.1 2051584.0 82632.6  2097152.0  1322928.7  213592.0 192352.5 25728.0 21515.7    538   17.258   7      2.904   20.162
21504.0 22528.0  0.0   8784.1 2051584.0 82632.6  2097152.0  1322928.7  213592.0 192352.5 25728.0 21515.7    538   17.258   7      2.904   20.162
21504.0 22528.0  0.0   8784.1 2051584.0 82632.6  2097152.0  1322928.7  213592.0 192352.5 25728.0 21515.7    538   17.258   7      2.904   20.162

这个命令有个妙用,就是通过设置间隔时间以及查询次数,可以大概分析JVM内存使用情况的哦。比如:

  • 测算年轻代对象增长的速率。可以执行命令 jstat -gc pid 1000 10 (每隔1秒执行1次命令,共执行10次),通过观察EU(eden区的使用)来估算每秒eden大概新增多少对象,如果系统负载不高,可以把频率1秒换成1分钟,甚至10分钟来观察整体情况。注意,一般系统可能有高峰期和日常期,所以需要在不同的时间分别估算不同情况下对象增长速率
  • 测算YoungGC的触发频率和每次耗时。通过上面我们已经知道了年轻代对象的增长速率了,那根据eden区的大小,除一下,不就得到大概的GC时间了嘛。另外,我们还可以通过YGCT/YGC公式算出YoungGC的平均耗时,我们就能大概推算出系统多久会因为Young GC的执行而卡顿多长时间
  • 测算每次Young GC后有多少对象存活和进入老年代。这个因为之前已经大概知道Young GC的频率,假设是每5分钟一次,那么可以执行命令 jstat -gc pid 300000 10 ,观察每次结果eden,survivor和老年代使用的变化情况,在每次gc后eden区使用一般会大幅减少,survivor和老年代都有可能增长,这些增长的对象就是每次Young GC后存活的对象,同时还可以看出每次Young GC后进去老年代大概多少对象,从而可以推算出老年代对象增长速率。
  • Full GC的触发频率和每次耗时。知道了老年代对象的增长速率就可以推算出Full GC的触发频率了,Full GC的每次耗时可以用公式 FGCT/FGC 计算得出。

优化思路其实简单来说就是尽量让每次Young GC后的存活对象小于Survivor区域的50%(避免触发对象动态年龄判断机制),都留存在年轻代里。尽量别让对象进入老年代。尽量减少Full GC的频率,避免频繁Full GC对JVM性能的影响。

问题

Q1:内存泄露到底是怎么回事
答:举个例子。一般电商架构可能会使用多级缓存架构,就是redis加上JVM级缓存,大多数同学可能为了图方便对于JVM级缓存就简单使用一个hashmap,于是不断往里面放缓存数据,但是很少考虑这个map的容量问题,结果这个缓存map越来越大,一直占用着老年代的很多空间,时间长了就会导致full gc非常频繁,这就是一种内存泄漏,对于一些老旧数据没有及时清理导致一直占用着宝贵的内存资源,时间长了除了导致full gc,还有可能导致OOM
这种情况完全可以考虑采用一些成熟的JVM级缓存框架来解决,比如ehcache等自带一些LRU数据淘汰算法的框架来作为JVM级的缓存

学习总结

  1. 学习了jps命令,用来查看当前运行的java应用程序的pid
  2. 学习了jmap命令,用来查看进程的内存信息。通过jmao -histo pid命令,可以帮助排查内存泄漏、大对象等异常
  3. 学习了jstack命, 用来查看线程状态信息、堆栈信息、锁信息等。通过jstack pid还可以定位线程死锁、死循环、内存泄漏等问题
  4. 学习了jinfo命令,用来查看当前进程的jvm参数状态,以及如何不重启修改jvm参数
  5. 学习了jstat命令,用来查看java的gc状态,用于推测业务gc模型做支撑

鸣谢

感谢【作者:学海无涯,行者无疆】的《java诊断与调优常用命令jmap、jstack、jstat使用实战》文章
感谢【作者:万猫学社】的《Java的jinfo命令使用详解》文章

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

验证码有毒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值