在面试的时候经常稳的JVM调优问题
- 线上环境,如果内存飙升了,应该怎么排查呢?
- 线上环境,如果CPU飙升了,应该怎么排查呢?
内存飙升首先要考虑是不是类有很多,并且没有被释放;使用jmap可以检查出哪个类很多
CPU飙升,可以使用Jstact 来找出CPU飙升的原因
下面就来研究Jmap,Jstact的用法。
目标:
- Jmap、Jstack、Jinfo详解
- JvisualVm调优工具实战
- JVM内存或CPU飙高如何定位
- JState命令预估JVM运行情况
- 系统频繁Full GC导致系统卡顿实战调优
- 内存泄漏到底是怎么回事?
一、前言
因为我的是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
打开工具界面如下:
![image-20211111175630231](/Users/luoxiaoli/Library/Application Support/typora-user-images/image-20211111175630231.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文件(内存很大的时候,可能会导不出来)
完整参数配置如下:
-Xms10M -Xmx10M -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/zhangsan/Downloads -XX:+HeapDumpOnOutOfMemoryError 表示的是内存溢出的时候输出文件 -XX:HeapDumpPath=/Users/zhangsan/Downloads 表示的是内存溢出的时候输出文件的路径
这里需要注意的是堆目录要写绝对路径,不能写相对路径。
第三步:启动项目,等待内存溢出
我们看到,运行没有多长时间就内存溢出了。
查看导出到文件的目录:
第四步:导入堆内存文件到jvisualvm工具
文件->装入->选择刚刚导出的文件
第五步:分析
我们主要看【类】这个模块。
通过上图我们可以明确看出,有三个类实例数特别多,分别是: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[]对象。
四、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(); } }
下面来分析一下这段代码:
- 定义了两个成员变量lock1,lock2
- main方法中定义了两个线程。
- 线程1内部使用的是同步执行--上锁,锁是lock1。休眠5秒钟之后,他要获取第二把锁,执行第二段代码。
- 线程2和线程1类似,锁相反。
- 问题:一开始,像个线程并行执行,线程一获取lock1,线程2获取lock2.然后线程1继续执行,当休眠5s后获取开启第二个同步执行,锁是lock2,但这时候很可能线程2还没有执行完,所以还没有释放lock2,于是等待。线程2刚开始获取了lock2锁,休眠五秒后要去获取lock1锁,这时lock1锁还没释放,于是等待。两个线程就处于相互等待中,造成死锁。
运行程序,通过Jstack命令来看看是否能检测到当前有死锁。
从这里面个异常可以看出,
- prio:当前线程的优先级
- cpu:cpu耗时
- os_prio:操作系统级别的优先级
- tid:线程id
- nid:系统内核的id
- state:当前的状态,BLOCKED,表示阻塞。通常正常的状态是Running我们看到Thread-0和Thread-1线程的状态都是BLOCKED.
通过上面的信息,我们判断出两个线程的状态都是BLOCKED,可能有点问题,然后继续往下看。
我们从最后的一段可以看到这句话: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工具。
找到当前运行的类,查看线程,就会看到最头上的一排红字:检测到死锁。然后点击“线程Dump”按钮,查看相信的线程死锁的信息。
这里可以找到线程私锁的详细信息,具体内容和上面使用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
我们看到cpu严重飙高,一般cpu达到80%就会报警了
第二步:使用top -p 命令查看飙高进程
使用【top -p 进程号】 查看进程id的cpu占用情况
第三步:按H,获取每个线程的内存情况
需要注意的是,这里的H是大写的H。
我们可以看出线程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: 表示的是线程号对应的十六进制数
通过这个方式可以查询到这个线程对应的堆栈信息
从这里我们可以看出有问题的线程id是0x4cd0, 哪一句代码有问题呢,Math类的22行。
第七步:查看对应的堆栈信息找出可能存在问题的代码
上述方法定位问题已经很精确了,接下来就是区代码里排查为什么会有问题了