jvm调优工具及案例分析 (上)

简介: jvm调优工具及案例分析

在面试的时候经常稳的JVM调优问题


  1. 线上环境,如果内存飙升了,应该怎么排查呢?
  2. 线上环境,如果CPU飙升了,应该怎么排查呢?


内存飙升首先要考虑是不是类有很多,并且没有被释放;使用jmap可以检查出哪个类很多


CPU飙升,可以使用Jstact 来找出CPU飙升的原因

下面就来研究Jmap,Jstact的用法。


目标:


  1. Jmap、Jstack、Jinfo详解
  2. JvisualVm调优工具实战
  3. JVM内存或CPU飙高如何定位
  4. JState命令预估JVM运行情况
  5. 系统频繁Full GC导致系统卡顿实战调优
  6. 内存泄漏到底是怎么回事?

一、前言


因为我的是mac电脑,所以运行程序都是在mac上,有时一些工具在mac上不是很好用。如果有不好用的情况,可以参考文章:


1. mac安装多版本jdk


2. 彻底解决Jmap在mac版本无法使用的问题


以上是我在mac上运行Jmap时遇到的问题,如果你也遇到了,可以查看。


二、Jmap使用



1. Jmap -histo 进程号


这个命令是用来查看系统内存使用情况的,实例个数,以及占用内存。


命令:


jmap -histo 3241

运行结果:

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

这里显示的是,byte类型的数组,有多少个实例,占用多大内存。


  • 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[][]


2. Jmap -heap 进程号


注意:Jmap命令在mac不太好用,具体参考前言部分。


windows或者linux上运行的命令是


Jmap -heap 进程号

mac上运行的命令是:(jdk8不能正常运行,jdk9以上可以)


jhsdb jmap --heap --pid 2139

执行结果


Attaching to process ID 2139, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 11.0.2+9
using thread-local object allocation.
Garbage-First (G1) GC with 8 thread(s)
Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 4294967296 (4096.0MB)
   NewSize                  = 1363144 (1.2999954223632812MB)
   MaxNewSize               = 2576351232 (2457.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  = 4096
   capacity = 4294967296 (4096.0MB)
   used     = 21654560 (20.651397705078125MB)
   free     = 4273312736 (4075.348602294922MB)
   0.5041845142841339% used
G1 Young Generation:
Eden Space:
   regions  = 15
   capacity = 52428800 (50.0MB)
   used     = 15728640 (15.0MB)
   free     = 36700160 (35.0MB)
   30.0% used
Survivor Space:
   regions  = 5
   capacity = 5242880 (5.0MB)
   used     = 5242880 (5.0MB)
   free     = 0 (0.0MB)
   100.0% used
G1 Old Generation:
   regions  = 1
   capacity = 210763776 (201.0MB)
   used     = 0 (0.0MB)
   free     = 210763776 (201.0MB)
   0.0% used

通过上述结果分析,我们查询的内容如下:


  • 进程号:2139
  • JDK版本号:11
  • 使用的垃圾收集器:G1(jdk11默认的)
  • G1垃圾收集器线程数:8
  • 还可以知道堆空间大小,已用大小,元数据空间大小等等。
  • 新生代,老年代region的大小。容量,已用,空闲等。


3. Jmap -dump 导出堆信息


这个命令是导出堆信息,当我们线上有内存溢出的情况的时候,可以使用Jmap -dump导出堆内存信息。然后再导入可视化工具用jvisualvm进行分析。


导出命令

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

我们还可以设置内存溢出自动导出dump文件(内存很大的时候,可能会导不出来)

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

下面有案例说明如何使用。


三、jvisualvm命令工具的使用



1. 基础用法


上面我们有导出dump堆信息到文件中,可以使用jvisualvm工具导入dump堆信息,进行分析。


打开jvisualvm工具命令:


jvisualvm

打开工具界面如下:


1187916-20211111175703272-1543445039.png

![image-20211111175630231](/Users/luoxiaoli/Library/Application Support/typora-user-images/image-20211111175630231.png)

1187916-20211109152752156-231356326.png


点击文件->装入,可以导入文件,查看系统的运行情况了。


2.案例分析 - 堆空间溢出问题定位


下面通过工具来分析内存溢出的原因。


第一步:自定义一段可能会内存溢出的代码,如下:


import com.aaa.jvm.User;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@SpringBootApplicationpublic class JVMApplication { 
    public static void main(String[] args) {    
        List<Object> list = new ArrayList<>();    
        int i = 0;    
        int j = 0;    
        while (true) {      
          list.add(new User(i++, UUID.randomUUID().toString()));      
          new User(j--, UUID.randomUUID().toString());    
        } 
    }
}

第二步:配置参数


为了方便看到效果,所以我们会设置两组参数。


第一组:设置堆空间大小,将堆空间设置的小一些,可以更快查看内存溢出的效果


‐Xms10M ‐Xmx10M ‐XX:+PrintGCDetails

设置的堆内存空间是10M,并且打印GC


第二组:设置内存溢出自动导出dump文件(内存很大的时候,可能会导不出来)


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

将这两组参数添加到项目启动配置中。


运行的过程中打印堆空间信息到文件中:


jmap -dump:file=a.dump,format=b 12152

后面我们可以使用工具导入堆文件进行分析(下面有说到)。


我们还可以设置内存溢出自动导出dump文件(内存很大的时候,可能会导不出来)

1187916-20211109160204998-1748859037.png

完整参数配置如下:


-Xms10M -Xmx10M -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/zhangsan/Downloads
-XX:+HeapDumpOnOutOfMemoryError 表示的是内存溢出的时候输出文件
-XX:HeapDumpPath=/Users/zhangsan/Downloads   表示的是内存溢出的时候输出文件的路径

这里需要注意的是堆目录要写绝对路径,不能写相对路径。


第三步:启动项目,等待内存溢出


我们看到,运行没有多长时间就内存溢出了。

1187916-20211109160428675-243610782.png

查看导出到文件的目录:


1187916-20211109162508154-1612933045.png


第四步:导入堆内存文件到jvisualvm工具


文件->装入->选择刚刚导出的文件

1187916-20211109162633570-2012498974.png

第五步:分析


我们主要看【类】这个模块。


1187916-20211109162759062-1753804867.png

通过上图我们可以明确看出,有三个类实例数特别多,分别是:byte[],java.lang.String,com.lxl.jvm.User。前两个我们不容易看出是哪里的问题,但是第三个类com.lxl.jvm.User我们就看出来了,问题出在哪里。接下来就重点排查调用了这个类的地方,有没有出现内存没有释放的情况。


这个程序很简单,那么byte[]和java.lang.String到底是什么呢?我们的User对象结构中字段类型是String。


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

既然有很多User,自然String也少不了。


那么byte[]是怎么回事呢?其实String类中有byte[]成员变量。所以也会有很多byte[]对象。

1187916-20211109163238046-418232692.png

四、Jstack使用



Jstack可以用来查看堆栈使用情况,还可以查看进程死锁情况。


4.1 [Jstack 进程号] 进程死锁分析


1.执行命令:


Jstack 进程号

2. 死锁案例分析:


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命令来看看是否能检测到当前有死锁。

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行。通过这个提示,我们就可以找出死锁在哪里了。


3. 使用jvisualvm查看死锁


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

1187916-20211110141819574-259304233.png


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


1187916-20211110142013212-1713712164.png

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


4.2 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();
        }
    }
}

这是一段死循环代码,会占满cpu。下面就运行这段代码,来看看如何排查cpu飙高的问题。


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

top

1187916-20211111171417511-1726137405.png


我们看到cpu严重飙高,一般cpu达到80%就会报警了


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


使用【top -p 进程号】 查看进程id的cpu占用情况

1187916-20211111171454174-53044259.png

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


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

1187916-20211111171523011-1875810060.png


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


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


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


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


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


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


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


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


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

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

1187916-20211111162304143-248076856.png


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


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


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

相关文章
|
29天前
|
监控 架构师 Java
Java虚拟机调优的艺术:从入门到精通####
本文作为一篇深入浅出的技术指南,旨在为Java开发者揭示JVM调优的神秘面纱,通过剖析其背后的原理、分享实战经验与最佳实践,引领读者踏上从调优新手到高手的进阶之路。不同于传统的摘要概述,本文将以一场虚拟的对话形式,模拟一位经验丰富的架构师向初学者传授JVM调优的心法,激发学习兴趣,同时概括性地介绍文章将探讨的核心议题——性能监控、垃圾回收优化、内存管理及常见问题解决策略。 ####
|
2月前
|
监控 算法 Java
jvm-48-java 变更导致压测应用性能下降,如何分析定位原因?
【11月更文挑战第17天】当JVM相关变更导致压测应用性能下降时,可通过检查变更内容(如JVM参数、Java版本、代码变更)、收集性能监控数据(使用JVM监控工具、应用性能监控工具、系统资源监控)、分析垃圾回收情况(GC日志分析、内存泄漏检查)、分析线程和锁(线程状态分析、锁竞争分析)及分析代码执行路径(使用代码性能分析工具、代码审查)等步骤来定位和解决问题。
|
2月前
|
Arthas Prometheus 监控
监控堆外使用JVM工具
监控堆外使用JVM工具
44 7
|
2月前
|
监控 Java 编译器
Java虚拟机调优指南####
本文深入探讨了Java虚拟机(JVM)调优的精髓,从内存管理、垃圾回收到性能监控等多个维度出发,为开发者提供了一系列实用的调优策略。通过优化配置与参数调整,旨在帮助读者提升Java应用的运行效率和稳定性,确保其在高并发、大数据量场景下依然能够保持高效运作。 ####
35 1
|
2月前
|
存储 算法 Java
JVM进阶调优系列(10)敢向stop the world喊卡的G1垃圾回收器 | 有必要讲透
本文详细介绍了G1垃圾回收器的背景、核心原理及其回收过程。G1,即Garbage First,旨在通过将堆内存划分为多个Region来实现低延时的垃圾回收,每个Region可以根据其垃圾回收的价值被优先回收。文章还探讨了G1的Young GC、Mixed GC以及Full GC的具体流程,并列出了G1回收器的核心参数配置,帮助读者更好地理解和优化G1的使用。
|
2月前
|
监控 Java 测试技术
Elasticsearch集群JVM调优垃圾回收器的选择
Elasticsearch集群JVM调优垃圾回收器的选择
59 1
|
2月前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
2月前
|
监控 Java 编译器
Java虚拟机调优实战指南####
本文深入探讨了Java虚拟机(JVM)的调优策略,旨在帮助开发者和系统管理员通过具体、实用的技巧提升Java应用的性能与稳定性。不同于传统摘要的概括性描述,本文摘要将直接列出五大核心调优要点,为读者提供快速预览: 1. **初始堆内存设置**:合理配置-Xms和-Xmx参数,避免频繁的内存分配与回收。 2. **垃圾收集器选择**:根据应用特性选择合适的GC策略,如G1 GC、ZGC等。 3. **线程优化**:调整线程栈大小及并发线程数,平衡资源利用率与响应速度。 4. **JIT编译器优化**:利用-XX:CompileThreshold等参数优化即时编译性能。 5. **监控与诊断工
|
2月前
|
存储 监控 Java
JVM进阶调优系列(8)如何手把手,逐行教她看懂GC日志?| IT男的专属浪漫
本文介绍了如何通过JVM参数打印GC日志,并通过示例代码展示了频繁YGC和FGC的场景。文章首先讲解了常见的GC日志参数,如`-XX:+PrintGCDetails`、`-XX:+PrintGCDateStamps`等,然后通过具体的JVM参数和代码示例,模拟了不同内存分配情况下的GC行为。最后,详细解析了GC日志的内容,帮助读者理解GC的执行过程和GC处理机制。
|
3月前
|
Arthas 监控 数据可视化
JVM进阶调优系列(7)JVM调优监控必备命令、工具集合|实用干货
本文介绍了JVM调优监控命令及其应用,包括JDK自带工具如jps、jinfo、jstat、jstack、jmap、jhat等,以及第三方工具如Arthas、GCeasy、MAT、GCViewer等。通过这些工具,可以有效监控和优化JVM性能,解决内存泄漏、线程死锁等问题,提高系统稳定性。文章还提供了详细的命令示例和应用场景,帮助读者更好地理解和使用这些工具。