前言
在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();
}
}
}
-
使用命令top -p <pid> ,显示你的java进程的内存情况,pid是你的java进程号,比如:19663
-
按H,获取每个线程的内存使用情况
-
找到内存和cpu占用最高的线程tid,比如:19664
-
转为十六进制得到 0x4cd0,此为线程id的十六进制表示
-
执行 jstack 19663|grep -A 10 4cd0,得到线程堆栈信息中 4cd0 这个线程所在行的后面10行,从堆栈中可以发现导致cpu飙高的调用方法
-
查看对应的堆栈信息找出可能存在问题的代码
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级的缓存
学习总结
- 学习了jps命令,用来查看当前运行的java应用程序的pid
- 学习了jmap命令,用来查看进程的内存信息。通过jmao -histo pid命令,可以帮助排查内存泄漏、大对象等异常
- 学习了jstack命, 用来查看线程状态信息、堆栈信息、锁信息等。通过jstack pid还可以定位线程死锁、死循环、内存泄漏等问题
- 学习了jinfo命令,用来查看当前进程的jvm参数状态,以及如何不重启修改jvm参数
- 学习了jstat命令,用来查看java的gc状态,用于推测业务gc模型做支撑
鸣谢
感谢【作者:学海无涯,行者无疆】的《java诊断与调优常用命令jmap、jstack、jstat使用实战》文章
感谢【作者:万猫学社】的《Java的jinfo命令使用详解》文章
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/180541.html