JVM调优工具锦囊 (上)

简介: JVM调优工具锦囊

Arthas线上


分析诊断调优工具


以前我们要排查线上问题,通常使用的是jdk自带的调优工具和命令。最常见的就是dump线上日志,然后下载到本地,导入到jvisualvm工具中。这样操作有诸多不变,现在阿里团队开发的Arhtas工具,拥有非常强大的功能,并且都是线上的刚需,尤其是情况紧急,不方便立刻发版,适合临时处理危急情况使用。下面分两部分来研究JVM性能调优工具:


1.JDK自带的性能调优工具

虽然有了Arthas,但也不要忘记JDK自带的性能调优工具,在某些场景下,他还是有很大作用的。而且Arthas里面很多功能其根本就是封装了JDK自带的这些调优命令。

2.Arthas线上分析工具的使用

这一部分,主要介绍几个排查线上问题常用的方法。功能真的很强大,刚兴趣的猿媛可以研究其基本原理。之前跟我同事讨论,感觉这就像病毒一样,可以修改内存里的东西,真的还是挺强大的。


以上两种方式排查线上问题,没有优劣之分,如果线上不能安装Arthas就是jdk自带命令,如果jdk自带命令不能满足部分要求,又可以安装Arthas,那就使用Arthas。他们只是排查问题的工具,重要的是排查问题的思路。不管黑猫、白猫,能抓住耗子就是好猫。


一、JDK自带的调优工具



这里不是流水一样的介绍功能怎么用,就说说线上遇到的问题,我们通常怎么排查,排查的几种情况。

  • 内存溢出,出现OutOfMemoryError,这个问题如何排查
  • CPU使用猛增,这个问题如何排查?
  • 进程有死锁,这个问题如何排查?
  • JVM参数调优


下面来一个一个解决


1、处理内存溢出,报OutOfMemoryError错误


第一步:通过jmap -histo命令查看系统内存使用情况


使用的命令:

jmap -histo 进程号

运行结果:

num     #instances         #bytes  class name
----------------------------------------------
   1:       1101980      372161752  [B
   2:        551394      186807240  [Ljava.lang.Object;
   3:       1235341      181685128  [C
   4:         76692      170306096  [I
   5:        459168       14693376  java.util.concurrent.locks.AbstractQueuedSynchronizer$Node
   6:        543699       13048776  java.lang.String
   7:        497636       11943264  java.util.ArrayList
   8:        124271       10935848  java.lang.reflect.Method
   9:        348582        7057632  [Ljava.lang.Class;
  10:        186244        5959808  java.util.concurrent.ConcurrentHashMap$Node
  8671:      1             16  zipkin2.reporter.Reporter$1
  8672:      1             16  zipkin2.reporter.Reporter$2
Total       8601492      923719424                                   
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[][]

通过这个命令,我们可以看出当前哪个对象最消耗内存。


上面这个运行结果是我启动了本地的一个项目,然后运行【jmap -histro 进程号】运行出来的结果,直接去了其中的一部分。通过这里我们可以看看大的实例对象中,有没有我们自定义的实例对象。通过这个可以排查出哪个实例对象引起的内存溢出。


除此之外,Total汇总数据可以看出当前一共有多少个对象,暂用了多大内存空间。这里是有约860w个对象,占用约923M的空间。


第二步:分析内存溢出,查看堆空间占用情况


使用命令


jhsdb jmap --heap --pid 进程号

比如,我本地启动了一个项目,想要查看这个项目的内存占用情况:


[root@iZ2pl8Z ~]# jhsdb jmap --heap --pid 28692
Attaching to process ID 28692, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 11.0.13+10-LTS-370
using thread-local object allocation.
Garbage-First (G1) GC with 4 thread(s)
Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 2065694720 (1970.0MB)
   NewSize                  = 1363144 (1.2999954223632812MB)
   MaxNewSize               = 1239416832 (1182.0MB)
   OldSize                  = 5452592 (5.1999969482421875MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 1048576 (1.0MB)
Heap Usage:
G1 Heap:
   regions  = 1970
   capacity = 2065694720 (1970.0MB)
   used     = 467303384 (445.65523529052734MB)
   free     = 1598391336 (1524.3447647094727MB)
   22.622093161955704% used
G1 Young Generation:
Eden Space:
   regions  = 263
   capacity = 464519168 (443.0MB)
   used     = 275775488 (263.0MB)
   free     = 188743680 (180.0MB)
   59.36794582392776% used
Survivor Space:
   regions  = 6
   capacity = 6291456 (6.0MB)
   used     = 6291456 (6.0MB)
   free     = 0 (0.0MB)
   100.0% used
G1 Old Generation:
   regions  = 179
   capacity = 275775488 (263.0MB)
   used     = 186285016 (177.65523529052734MB)
   free     = 89490472 (85.34476470947266MB)
   67.54951912187352% used

下面来看看参数的含义


堆空间配置信息


Heap Configuration:
  /**
   * 空闲堆空间的最小百分比,计算公式为:HeapFreeRatio =(CurrentFreeHeapSize/CurrentTotalHeapSize) * 100,值的区间为0   * 到100,默认值为 40。如果HeapFreeRatio < MinHeapFreeRatio,则需要进行堆扩容,扩容的时机应该在每次垃圾回收之后。
   */
  MinHeapFreeRatio = 40  
  /**
   * 空闲堆空间的最大百分比,计算公式为:HeapFreeRatio =(CurrentFreeHeapSize/CurrentTotalHeapSize) * 100,值的区间为0  
   * 到100,默认值为 70。如果HeapFreeRatio > MaxHeapFreeRatio,则需要进行堆缩容,缩容的时机应该在每次垃圾回收之后
   */
  MaxHeapFreeRatio         = 70
  /**JVM 堆空间允许的最大值*/
  MaxHeapSize              = 2065694720 (1970.0MB)
  /** JVM 新生代堆空间的默认值*/
  NewSize                  = 1363144 (1.2999954223632812MB)
  /** JVM 新生代堆空间允许的最大值 */
  MaxNewSize               = 1239416832 (1182.0MB)
  /** JVM 老年代堆空间的默认值 */
  OldSize                  = 5452592 (5.1999969482421875MB)
  /** 新生代(2个Survivor区和Eden区 )与老年代(不包括永久区)的堆空间比值,表示新生代:老年代=1:2*/
  NewRatio                 = 2
  /** 两个Survivor区和Eden区的堆空间比值为 8,表示 S0 : S1 :Eden = 1:1:8 */
  SurvivorRatio            = 8
  /** JVM 元空间的默认值 */
  MetaspaceSize            = 21807104 (20.796875MB)
  CompressedClassSpaceSize = 1073741824 (1024.0MB)
  /** JVM 元空间允许的最大值 */
  MaxMetaspaceSize         = 17592186044415 MB
  /** 在使用 G1 垃圾回收算法时,JVM 会将 Heap 空间分隔为若干个 Region,该参数用来指定每个 Region 空间的大小 */
  G1HeapRegionSize         = 1048576 (1.0MB)

G1堆使用情况


Heap Usage:
G1 Heap:
   regions  = 1970
   capacity = 2065694720 (1970.0MB)
   used     = 467303384 (445.65523529052734MB)
   free     = 1598391336 (1524.3447647094727MB)
   22.622093161955704% used
G1 的 Heap 使用情况,该 Heap 包含 1970 个 Region,结合上文每个 RegionSize=1M,因此 Capacity = Regions * RegionSize = 1970 * 1M = 1970M,已使用空间为 445.65M,空闲空间为 1524.34M,使用率为 22.62%。

G1年轻代Eden区使用情况


G1 Young Generation:
Eden Space:
   regions  = 263
   capacity = 464519168 (443.0MB)
   used     = 275775488 (263.0MB)
   free     = 188743680 (180.0MB)
   59.36794582392776% used
G1 的 Eden 区的使用情况,总共使用了 263 个 Region,结合上文每个 RegionSize=1M,因此 Used = Regions * RegionSize = 263 * 1M = 263M,Capacity=443M 表明当前 Eden 空间分配了 443 个 Region,使用率为 59.37%。

G1年轻代Survivor区使用情况和G1老年代使用情况:和Eden区类似


Survivor Space:
   regions  = 6
   capacity = 6291456 (6.0MB)
   used     = 6291456 (6.0MB)
   free     = 0 (0.0MB)
   100.0% used
G1 Old Generation:
   regions  = 179
   capacity = 275775488 (263.0MB)
   used     = 186285016 (177.65523529052734MB)
   free     = 89490472 (85.34476470947266MB)
   67.54951912187352% used
Survivor区使用情况和Eden区类似。 老年代参数含义和Eden区类似。

通过上面的命令,我们就能知道当前系统堆空间的使用情况了,到底是老年代有问题还是新生代有问题。


第三步:导出dump内存溢出的文件,导入到jvisualvm查看


如果前两种方式还是没有排查出问题,我们可以导出内存溢出的日志,在导入客户端进行分析

使用的命令是:


jmap -dump:file=a.dump 进程号

或者是直接设置JVM参数


-XX:+HeapDumpOnOutOfMemoryError 
-XX:HeapDumpPath=./ (路径)

然后导入到jvisualvm中进行分析,方法是:点击文件->装入,导入文件,查看系统的运行情况了。

1187916-20220106165853866-92127438.png


通过分析实例数,看看哪个对象实例占比最高,这里重点看我们自定义的类,然后分析这个对象里面有没有大对象,从而找出引起内存溢出的根本原因。


2、CPU使用猛增,这个问题如何排查?


我们可以通过Jstack找出占用cpu最高的线程的堆栈信息,下面来一步一步分析。

假设我们有一段死循环,不断执行方法调用,线程始终运行不释放就会导致CPU飙高,示例代码如下:


package com.lxl.jvm;
public class Math {
    public static int initData = 666;
    public static User user = new User();
    public User user1;
    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();
        }
    }
}


第一步:运行代码,使用top命令查看cpu占用情况

image.png

如上,现在有一个java进程,cpu严重飙高了,接下来如何处理呢?


第二步:使用top -p 命令查看飙高进程


top -p 46518

1187916-20220106171653101-1436418068.png


我们看到了单独的46518这个线程的详细信息


第三步:按H,获取每个线程的内存情况


需要注意的是,这里的H是大写的H。

1187916-20211111171523011-1875810060.png


我们可以看出线程0和线程1线程号飙高。


第四步:找到内存和cpu占用最高的线程tid


通过上图我们看到占用cpu资源最高的线程有两个,线程号分别是4018362,4018363。我们一第一个为例说明,如何查询这个线程是哪个线程,以及这个线程的什么地方出现问题,导致cpu飙高。


第五步:将线程tid转化为十六进制


67187778是线程号为4013442的十六进制数。具体转换可以网上查询工具。


第六步:执行[ jstack 4018360|grep -A 10 67187778] 查询飙高线程的堆栈信息


接下来查询飙高线程的堆栈信息


jstack 4013440|grep -A 10 67190882
  • 4013440:表示的是进程号


  • 67187778: 表示的是线程号对应的十六进制数


通过这个方式可以查询到这个线程对应的堆栈信息

1187916-20211111162304143-248076856.png

从这里我们可以看出有问题的线程id是0x4cd0, 哪一句代码有问题呢,Math类的22行。


第七步:查看对应的堆栈信息找出可能存在问题的代码


上述方法定位问题已经很精确了,接下来就是区代码里排查为什么会有问题了。


备注:上面的进程id可能没有对应上,在测试的时候,需要写对进程id和线程id


3、进程有死锁,这个问题如何排查?


Jstack可以用来查看堆栈使用情况,以及进程死锁情况。下面就来看看如何排查进程死锁

还是通过案例来分析


package com.lxl.jvm;
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();
    }
}

上面是两把锁,互相调用。


  1. 定义了两个成员变量lock1,lock2
  2. main方法中定义了两个线程。
  • 线程1内部使用的是同步执行--上锁,锁是lock1。休眠5秒钟之后,他要获取第二把锁,执行第二段代码。
  • 线程2和线程1类似,锁相反。
  1. 问题:一开始,像个线程并行执行,线程一获取lock1,线程2获取lock2.然后线程1继续执行,当休眠5s后获取开启第二个同步执行,锁是lock2,但这时候很可能线程2还没有执行完,所以还没有释放lock2,于是等待。线程2刚开始获取了lock2锁,休眠五秒后要去获取lock1锁,这时lock1锁还没释放,于是等待。两个线程就处于相互等待中,造成死锁。


第一步:通过Jstack命令来看看是否能检测到当前有死锁。


jstack 51789
JAVA 复制 全屏

1187916-20211110113858479-211013813.png

从这里面个异常可以看出,


  • prio:当前线程的优先级
  • cpu:cpu耗时
  • os_prio:操作系统级别的优先级
  • tid:线程id
  • nid:系统内核的id
  • state:当前的状态,BLOCKED,表示阻塞。通常正常的状态是Running我们看到Thread-0和Thread-1线程的状态都是BLOCKED.


通过上面的信息,我们判断出两个线程的状态都是BLOCKED,可能有点问题,然后继续往下看。

1187916-20211110114534052-1818728564.png

我们从最后的一段可以看到这句话:Found one Java-level deadlock; 意思是找到一个死锁。死锁的线程号是Thread-0,Thread-1。


Thread-0:正在等待0x000000070e706ef8对象的锁,这个对象现在被Thread-1持有。

Thread-1:正在等待0x000000070e705c98对象的锁,这个对象现在正在被Thread-0持有。


最下面展示的是死锁的堆栈信息。死锁可能发生在DeadLockTest的第17行和第31行。通过这个提示,我们就可以找出死锁在哪里了。


第二步:使用jvisualvm查看死锁


如果使用jstack感觉不太方便,还可以使用jvisualvm,通过界面来查看,更加直观。

在程序代码启动的过程中,打开jvisualvm工具。

1187916-20211110141819574-259304233.png

找到当前运行的类,查看线程,就会看到最头上的一排红字:检测到死锁。然后点击“线程Dump”按钮,查看相信的线程死锁的信息。

1187916-20211110142013212-1713712164.png

这里可以找到线程私锁的详细信息,具体内容和上面使用Jstack命令查询的结果一样,这里实用工具更加方便。


4、JVM参数调优


jvm调优通常使用的是Jstat命令。


1. 垃圾回收统计 jstat -gc


jstat -gc 进程id

这个命令非常常用,在线上有问题的时候,可以通过这个命令来分析问题。


下面我们来测试一下,启动一个项目,然后在终端驶入jstat -gc 进程id,得到如下结果:

1187916-20211112164736562-1107523060.png


上面的参数分别是什么意思呢?先识别参数的含义,然后根据参数进行分析


  • S0C: 第一个Survivor区的容量
  • S1C: 第二个Survivor区的容量
  • S0U: 第一个Survivor区已经使用的容量
  • S1U:第二个Survivor区已经使用的容量
  • EC: 新生代Eden区的容量
  • EU: 新生代Eden区已经使用的容量
  • OC: 老年代容量
  • OU:老年代已经使用的容量
  • MC: 方法区大小(元空间)
  • MU: 方法区已经使用的大小
  • CCSC:压缩指针占用空间
  • CCSU:压缩指针已经使用的空间
  • YGC: YoungGC已经发生的次数
  • YGCT: 这一次YoungGC耗时
  • FGC: Full GC发生的次数
  • FGCT: Full GC耗时
  • GCT: 总的GC耗时,等于YGCT+FGCT


连续观察GC变化的命令


jstat -gc 进程ID 间隔时间  打印次数

举个例子:我要打印10次gc信息,每次间隔1秒


jstat -gc 进程ID 1000 10

1187916-20211112171023928-44170232.png

这样就连续打印了10次gc的变化,每次隔一秒。


这个命令是对整体垃圾回收情况的统计,下面将会差分处理。


2.堆内存统计


这个命令是打印堆内存的使用情况。


jstat -gccapacity 进程ID

1187916-20211112174139395-715365764.png


  • NGCMN:新生代最小容量
  • NGCMX:新生代最大容量
  • NGC:当前新生代容量
  • S0C:第一个Survivor区大小
  • S1C:第二个Survivor区大小
  • EC:Eden区的大小
  • OGCMN:老年代最小容量
  • OGCMX:老年代最大容量
  • OGC:当前老年代大小
  • OC: 当前老年代大小
  • MCMN: 最小元数据容量
  • MCMX:最大元数据容量
  • MC:当前元数据空间大小
  • CCSMN:最小压缩类空间大小
  • CCSMX:最大压缩类空间大小
  • CCSC:当前压缩类空间大小
  • YGC:年轻代gc次数
  • FGC:老年代GC次数


3.新生代垃圾回收统计


命令:


jstat -gcnew 进程ID [ 间隔时间  打印次数]


这个指的是当前某一次GC的内存情况

1187916-20211112174721317-912134699.png


  • S0C:第一个Survivor的大小
  • S1C:第二个Survivor的大小
  • S0U:第一个Survivor已使用大小
  • S1U:第二个Survivor已使用大小
  • TT: 对象在新生代存活的次数
  • MTT: 对象在新生代存活的最大次数
  • DSS: 期望的Survivor大小
  • EC:Eden区的大小
  • EU:Eden区的使用大小
  • YGC:年轻代垃圾回收次数
  • YGCT:年轻代垃圾回收消耗时间


4. 新生代内存统计


jstat -gcnewcapacity 进程ID

1187916-20211112180001851-1158415520.png


参数含义:


  • NGCMN:新生代最小容量
  • NGCMX:新生代最大容量
  • NGC:当前新生代容量
  • S0CMX:Survivor 1区最大大小
  • S0C:当前Survivor 1区大小
  • S1CMX:Survivor 2区最大大小
  • S1C:当前Survivor 2区大小
  • ECMX:最大Eden区大小
  • EC:当前Eden区大小
  • YGC:年轻代垃圾回收次数
  • FGC:老年代回收次数
相关文章
|
18天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
16天前
|
监控 Java 编译器
Java虚拟机调优实战指南####
本文深入探讨了Java虚拟机(JVM)的调优策略,旨在帮助开发者和系统管理员通过具体、实用的技巧提升Java应用的性能与稳定性。不同于传统摘要的概括性描述,本文摘要将直接列出五大核心调优要点,为读者提供快速预览: 1. **初始堆内存设置**:合理配置-Xms和-Xmx参数,避免频繁的内存分配与回收。 2. **垃圾收集器选择**:根据应用特性选择合适的GC策略,如G1 GC、ZGC等。 3. **线程优化**:调整线程栈大小及并发线程数,平衡资源利用率与响应速度。 4. **JIT编译器优化**:利用-XX:CompileThreshold等参数优化即时编译性能。 5. **监控与诊断工
|
27天前
|
存储 监控 Java
JVM进阶调优系列(8)如何手把手,逐行教她看懂GC日志?| IT男的专属浪漫
本文介绍了如何通过JVM参数打印GC日志,并通过示例代码展示了频繁YGC和FGC的场景。文章首先讲解了常见的GC日志参数,如`-XX:+PrintGCDetails`、`-XX:+PrintGCDateStamps`等,然后通过具体的JVM参数和代码示例,模拟了不同内存分配情况下的GC行为。最后,详细解析了GC日志的内容,帮助读者理解GC的执行过程和GC处理机制。
|
2月前
|
Arthas 监控 数据可视化
JVM进阶调优系列(7)JVM调优监控必备命令、工具集合|实用干货
本文介绍了JVM调优监控命令及其应用,包括JDK自带工具如jps、jinfo、jstat、jstack、jmap、jhat等,以及第三方工具如Arthas、GCeasy、MAT、GCViewer等。通过这些工具,可以有效监控和优化JVM性能,解决内存泄漏、线程死锁等问题,提高系统稳定性。文章还提供了详细的命令示例和应用场景,帮助读者更好地理解和使用这些工具。
|
2月前
|
监控 架构师 Java
JVM进阶调优系列(6)一文详解JVM参数与大厂实战调优模板推荐
本文详述了JVM参数的分类及使用方法,包括标准参数、非标准参数和不稳定参数的定义及其应用场景。特别介绍了JVM调优中的关键参数,如堆内存、垃圾回收器和GC日志等配置,并提供了大厂生产环境中常用的调优模板,帮助开发者优化Java应用程序的性能。
|
2月前
|
存储 监控 算法
JVM调优深度剖析:内存模型、垃圾收集、工具与实战
【10月更文挑战第9天】在Java开发领域,Java虚拟机(JVM)的性能调优是构建高性能、高并发系统不可或缺的一部分。作为一名资深架构师,深入理解JVM的内存模型、垃圾收集机制、调优工具及其实现原理,对于提升系统的整体性能和稳定性至关重要。本文将深入探讨这些内容,并提供针对单机几十万并发系统的JVM调优策略和Java代码示例。
55 2
|
2月前
|
Java
JVM进阶调优系列(5)CMS回收器通俗演义一文讲透FullGC
本文介绍了JVM中CMS垃圾回收器对Full GC的优化,包括Stop the world的影响、Full GC触发条件、GC过程的四个阶段(初始标记、并发标记、重新标记、并发清理)及并发清理期间的Concurrent mode failure处理,并简述了GC roots的概念及其在GC中的作用。
|
2月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
38 4
|
16天前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
16 1
|
2月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
72 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS