1. 介绍
市面上调优工具无非就是JDK基础调优命令的整合,这边调优使用JDK1.8进行。
2. JDK自带各种命令调优化
2.1. 前置准备
如果启动一个应用程序在运行,可用jps查看其进程id,接着就可以用JDK自带各种命令优化应用。
用jps查看JAVA进程的进程ID。
2.2. Jmap命令
此命令可以用来查看内存信息,实例个数以及占用内存大小。
2.2.1. 查看运行进程生成过的实例和存活实例
主要命令
jmap -histo 32984 #查看当前进程历史生成的实例
jmap -histo 32984 >./log.txt #输出到日志中
jmap -histo:live 32984 #查看当前进程,存活的实例,执行过程中可能会触发一次full gc
输出结果格式如下:
1. num:序号。
2. instances:实例数量。
3. bytes:这些实例总共占用空间大小字节。
4. classname:类名称。
2.2.2. 查看堆信息
命令
jmap -heap 32984
Heap Configuration表示堆的信息,可以查看最大堆内存,新生代的最大空间,新生代空间,老年代空间等。
Heap Usage堆内存的使用情况,比如Eden Space可以看Eden区的大小,使用的情况,空闲的情况。
2.2.3. 堆内存dump
jmap -dump:format=b,file=eureka.hprof 32984
生成当前堆信息的快照在执行命令的当前目录下,导出的dump文件可以用可视化工具查看,比如jvisualvm。
2.2.3.1. jvisualvm命令工具导入该dump文件分析
启动jvisualvm—文件—装载选择生成的dump文件。
显示如下
2.2.3.2. 设置内存溢出自动导出dump文件
可以设置JVM启动参数,服务内存溢出时候自动导出dump文件。
1. -XX:+HeapDumpOnOutOfMemoryError。。
2. -XX:HeapDumpPath=./(导出的路径,可以绝对或者相对路径)。
示例代码:
package com.liu.jvm_tiao_you;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
public class Test1 {
public static List<Object> list = new ArrayList<>();
// JVM设置
// -Xms1M -Xmx1M -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D:\jvm.dump
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());
}
}
@Data
public static class User{
public User(int i, String name){
this.id = i;
this.name=name;
}
private int id;
private String name;
}
}
运行报oom,这时候自动导出dump,jvisualvm工具查看dump,可以查看哪些对象实例多的说明该对象创建处有问题。
注意
1. char数组比较多,是因为字符串内部使用的是char数组。
2.3. Jstack
2.3.1. 用Jstack加进程id查找死锁
1. 启动下面代码,造成死锁。
package com.liu.jvm_tiao_you;
/**
* @author jsLiu
* @version 1.0.0
* @description Jstack查看死锁的问题
* @date 2023/09/27
*/
public class Test2 {
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");
}
}
2. jps查看java进程id。
3. jstack 20344 查找该进行id下所有线程堆栈信息。
Thread-1—线程名
prio—线程的优先级
os_prio—操作系统内核的线程的优先级
tid—java的线程id
nid—线程对应的内核线程的id
java.lang.Thread.State: BLOCKED—线程的状态
如果有死锁会提示死锁信息。
Found one Java-level deadlock,表示发现死锁,后面信息表示线程Thread-1获取锁需要等待线程Thread-0的锁释放。
4. 利用 jvisualvm也能查看死锁。这些调优工具,只是对这些查看命令的封装。
流程:启动 jvisualvm—线程—线程Dump。
2.3.2. Jstack找出占用cpu最高的线程堆栈信息
操作系统是linux下。
1. 示例代码
package com.liu.jvm_tiao_you.jstack;
import lombok.Data;
/**
* @author jsLiu
* @version 1.0.0
* @description jstack 查看cpu飙升例子
* @date 2023/10/01
*/
public class Test2 {
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) {
Test2 test2 = new Test2();
while (true){
test2.compute();
}
}
@Data
public static class User{
public User(){
}
}
}
2. 使用命令top -p <pid>
,显示你的java进程的内存情况,pid是你的java进程号,比如19663。
3. 查看高负载进程下的高负载线程 按H(记住是大H,shift+h),获取每个线程的内存情况 (注意获取的是该进程下的每个线程)。
4. 找到内存和cpu占用最高的线程pid,比如19664,对应是操作系统内核的线程id。转为十六进制得到 0x4cd0,此为线程id的十六进制表示。
5. 执行 jstack 该线程下的进程id|grep -A 10 该线程id
。比如jstack 19663|grep -A 10 4cd0(注意小写),得到线程堆栈信息中4cd0这个线程,飙高关键行的后面10行信息,从堆栈中可以发现导致cpu飙高的调用方法。
6. 查看对应的堆栈信息找出可能存在问题的代码。
2.4. Jinfo
该命令查看正在运行的Java应用程序的扩展参数 。查看jvm的参数,查看java系统参数。
命令
jinfo [-命令选项] [进程id]。
1. 查看jvm启动的参数
jinfo -flags pid(线程id)
2. 查看java系统参数
jinfo -sysprops(线程id)
2.5. Jstat
比较重要的调优命令。jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。
命令
jstat命令命令格式:
jstat [Options] pid [interval] [count]
命令参数说明:
Options,一般使用 -gcutil 或 -gc 查看gc 情况
pid,当前运行的 java进程号
interval,间隔时间,单位为秒或者毫秒
count,打印次数,如果缺省则打印无数次
Options 参数如下:
-gc:统计 jdk gc时 heap信息,以使用空间字节数表示
-gcutil:统计 gc时, heap情况,以使用空间的百分比表示
-class:统计 class loader行为信息
-compile:统计编译行为信息
-gccapacity:统计不同 generations(新生代,老年代,持久代)的 heap容量情况
-gccause:统计引起 gc的事件
-gcnew:统计 gc时,新生代的情况
-gcnewcapacity:统计 gc时,新生代 heap容量
-gcold:统计 gc时,老年代的情况
-gcoldcapacity:统计 gc时,老年代 heap容量
-gcpermcapacity:统计 gc时, permanent区 heap容量
1. 垃圾回收统计
命令
jstat -gc pid(线程id)
命令后续可以跟参数 比如 jstat -gc pid 1000 10
代表每间隔一秒执行1次,共10次。
最常用,可以评估程序内存使用及GC压力整体情况。
1. S0C:第一个S区的大小,单位KB。
2. S1C:第二个S区的大小。
3. S0U:第一个S区的使用大小。
4. S1U:第二个S区的使用大小。
5. EC:Eden区的大小。
6. EU:Eden区的使用大小。
7. OC:老年代大小。
8.OU:老年代使用大小。
9. MC:方法区大小(元空间)。
10. MU:方法区使用大小。
11. CCSC:压缩类空间大小。
12. CCSU:压缩类空间使用大小。
13. YGC:年轻代垃圾回收次数。
14. YGCT:年轻代垃圾回收总共消耗时间,单位s。
15. FGC:老年代垃圾回收次数 。
16. FGCT:老年代垃圾回收总共消耗时间,单位s。
17. GCT:垃圾回收消耗总时间,单位s。
2. 堆内存统计
命令
jstat -gccapacity pid (线程id)
最常用
1. NGCMN:新生代最小容量。
2. NGCMX:新生代最大容量。
3. NGC:当前新生代的容量。
4. S0C:第一个S区大小。
5. S1C:第二个S区的大小。
6. EC:Eden区的大小。
7. OGCMN:老年代最小容量。
8. OGCMX:老年代最大容量。
9. OGC:当前老年代大小。
10. OC: 当前老年代大小。
11. MCMN: 最小元空间容量。
12. MCMX:最大元空间容量。
13. MC:当前元空间大小。
14. CCSMN:最小压缩类空间大小。
15. CCSMX:最大压缩类空间大小。
16. CCSC:当前压缩类空间大小。
17. YGC:年轻代GC次数。
18. FGC:老年代GC次数。
3. 新生代垃圾回收统计
命令
jstat - gcnew pid (线程id)
1. S0C:第一个S区的大小。
2. S1C:第二个S区的大小。
3. S0U:第一个S区的使用大小。
4. S1U:第二个S区的使用大小。
5. TT: 对象在新生代存活的次数。
6. MTT: 对象在新生代存活的最大次数。
7. DSS: 期望的S区大小。
8. EC:Eden区的大小。
9. EU:Eden区的使用大小。
10. YGC:年轻代垃圾回收次数。
11. YGCT:年轻代垃圾回收消耗时间。
- 新生代内存统计
jstat - gc newcapacity pid (线程id)
1. NGCMN:新生代最小容量。
2. NGCMX:新生代最大容量。
3. NGC:当前新生代容量。
4. S0CMX:第一个S区最大大小。
5. S0C:第一个S区大小。
6. S1CMX:第二个S2区最大大小。
7. S1C:第二个S2区最大大小。
8. ECMX:Eden区最大大小。
9. EC:当前Eden区大小。
10. YGC:年轻代垃圾回收次数。
11. FGC:老年代回收次数。
5. 老年代垃圾回收统计
jstat -gcold pid (线程id)
1. MC:方法区大小。
2. MU:方法区使用大小。
3. CCSC:压缩类空间大小(比如压缩指针)。
4. CCSU:压缩类空间使用大小。
5. OC:老年代大小。
6. OU:老年代使用大小。
7. YGC:年轻代垃圾回收次数。
8. FGC:老年代垃圾回收次数。
9. FGCT:老年代垃圾回收消耗时间。
10. GCT:垃圾回收消耗总时间。
6. 老年代内存统计
jstat -gcoldcapacity pid(线程id)
1. OGCMN:老年代最小容量。
2. OGCMX:老年代最大容量。
3. OGC:当前老年代大小。
4. OC:老年代大小。
5. YGC:年轻代垃圾回收次数。
6. FGC:老年代垃圾回收次数。
7. FGCT:老年代垃圾回收消耗时间。
8. GCT:垃圾回收消耗总时间。
7. 元数据空间统计
jstat -gcmetacapacity pid(线程id)
1. MCMN:最小元数据容量。
2. MCMX:最大元数据容量。
3. MC:当前元数据空间大小
4. CCSMN:最小压缩类空间大小。
5. CCSMX:最大压缩类空间大小。
6. CCSC:当前压缩类空间大小。
7. YGC:年轻代垃圾回收次数。
8. FGC:老年代垃圾回收次数。
9. FGCT:老年代垃圾回收消耗时间。
10. GCT:垃圾回收消耗总时间。
8. 统计堆使用比例和垃圾回收次数
jstat -gcutil pid(线程id)
1. S0:第一个S区当前使用比例。
2. S1:第二个S区当前使用比例。
3. E:Eden区使用比例。
4. O:老年代使用比例。
5. M:元数据区使用比例。
6. CCS:压缩使用比例。
7. YGC:年轻代垃圾回收次数。
8. FGC:老年代垃圾回收次数。
9. FGCT:老年代垃圾回收消耗时间。
10. GCT:垃圾回收消耗总时间。
2. jvisualvm进行远程连接调试
如果远程服务器开启了jvm进程,可以通过jvisualvm进行远程链接,生产不可能这么用。
运行程序时候需要注册端口和连接地址,才能用jvisualvm进行远程连接(其实使用是jmx的rmi方式连接)。
如下面借助jar包启动一个程序注册端口和连接地址, jvisualvm就能进行远程连接调试。
-Dcom.sun.management.jmxremote.port 为远程机器的JMX端口
-Djava.rmi.server.hostname 为远程机器IP
java -Dcom.sun.management.jmxremote.port=8888 -Djava.rmi.server.hostname=192.168.65.60 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -jar microservice-eureka-server.jar
tomcat情况下的JMX配置:在catalina.sh文件里的最后一个JAVA_OPTS的赋值语句下一行增加如下配置行。记住是赋值语句后面。
JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote.port=8888 -Djava.rmi.server.hostname=192.168.50.60 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false"
连接时记得确认下端口是否通畅,可以临时关闭下防火墙或者开放端口
systemctl stop firewalld #临时关闭防火墙
3. JVM运行情况预估
用 jstat gc -pid 命令可以计算出如下一些关键数据,有了这些数据就可以采用之前介绍过的优化思路,先给自己的系统设置一些初始性的JVM参数,比如堆内存大小,年轻代大小,Eden和Survivor的比例,老年代的大小,大对象的阈值,大龄对象进入老年代的阈值等。
3.1. 查看年轻代对象增长的速率
可以执行命令 jstat -gc pid 1000 10 (每隔1秒执行1次命令,共执行10次),通过观察EU(eden区的使用)来估算每秒eden大概新增多少对象,如果系统负载不高,可以把频率1秒换成1分钟,甚至10分钟来观察整体情况。注意,一般系统可能有高峰期和日常期,所以需要在不同的时间分别估算不同情况下对象增长速率。
3.2.查看Young GC的触发频率和每次耗时
知道年轻代对象增长速率就能推根据eden区的大小推算出Young GC大概多久触发一次,Young GC的平均耗时可以通过 YGCT/YGC 公式算出,大概就能知道系统大概多久会因为Young GC的执行而卡顿多久。
3.3. 每次Young GC后有多少对象存活和进入老年代
这个因为之前已经大概知道Young GC的频率,假设是每5分钟一次,那么可以执行命令 jstat -gc pid 300000 10 ,观察每次结果eden,survivor和老年代使用的变化情况,在每次gc后eden区使用一般会大幅减少,survivor和老年代都有可能增长,这些增长的对象就是每次Young GC后存活的对象,同时还可以看出每次Young GC后进去老年代大概多少对象,从而可以推算出老年代对象增长速率
3.4. Full GC的触发频率和每次耗时
知道了老年代对象的增长速率就可以推算出Full GC的触发频率了,Full GC的每次耗时可以用公式 FGCT/FGC 计算得出。
优化思路其实简单来说就是尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。尽量别让对象进入老年代。尽量减少Full GC的频率,避免频繁Full GC对JVM性能的影响。
4. 系统频繁Full GC导致系统卡顿分析例子
机器配置:2核4G。
JVM内存大小:2G
系统运行时间:7天
期间发生的Full GC次数和耗时:500多次,200多秒
期间发生的Young GC次数和耗时:1万多次,500多秒
大致算下来每天会发生70多次Full GC,平均每小时3次(正常几天一次),每次Full GC在400毫秒左右。
每天会发生1000多次Young GC,每分钟会发生1次,每次Young GC在50毫秒左右。
JVM参数设置如下
1. -Xms设置堆初始大小。
2. -Xmx设置堆最大大小。
3. -Xmn设置新生代的代谢。
4. -Xss设置每个线程的线程栈大小。
5. -XX:SurvivorRatio设置一个s区占Eden区的比例。
6. -XX:MetaspaceSize设置元空间大小
7. -XX:MaxMetaspaceSize元空间最大大小。
8. -XX:+UseParNewGC 年轻代垃圾回收使用ParNew。
9. -XX:+UseConcMarkSweepGC 代表启用CMS收集老年代
10. -XX:CMSInitiatingOccupancyFraction:当老年代使用达到该比例时会触发OldGC(CMS的GC,避免并发失败)。
11. -XX:+UseCMSInitiatingOccupancyOnly:开启表示只使用设定的回收阈值( 配合参数-XX:CMSInitiatingOccupancyFraction设定的值使用)。如果不开启,JVM仅在第一次使用设定值,后续则会自动调整。
-Xms1536M -Xmx1536M -Xmn512M -Xss256K -XX:SurvivorRatio=6 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly
**这边使用的是CMS垃圾收集器,老年代满其实做的是Old GC只不过习惯叫Full GC,严格来说Full GC触发跟垃圾回收器有关,比如CMS并发模式
分析
结合上面运行情况,计算平均值,不用考虑峰值情况,示意图如下。
每分钟一次Young GC可知Eden区一分钟满一次,每二十分钟发生一次Old GC推测老年代二十分钟满一次,根据设置条件70%时候就算满了,所以一分钟有700多M对象进入老年代。
结合对象挪动到老年代那些规则推理下这个程序可能存在的一些问题。
1. 大对象直接进入老年代,基本熟悉项目会知道,首先排除。
2. 长期存活对象进入老年代,基本都是缓存那些对象,基本不肯会增加,一般的对象都是GC时候清理掉,不可能放到老年代。
3. 假设系统压力大,每次Young GC 前几秒的创建的对象还被引用着(比如还没完整创建完,比如根据业务赋值等),则推测Young GC后就有30多M进入S区。在新生代GC后这块S区存活对象大于当前区域50%则就会触发动态年龄判断机制,这批对象进入老年代。
4. 再看老年代空间担保机制,基本不可能。
经过分析感觉可能是由于对象动态年龄判断机制导致Old GC较为频繁。
对应示例
这边不好传示例代码,大体看个分析流程。,配置上面jvm参数,启动程序之后根据进程pid,打印jstat的结果如下。
jstat -gc 13456 2000 10000
字段数据太长会出现偏移,别看错。
分析
启动一次做一次YounGC和Full GC很正常。
对于对象动态年龄判断机制导致的Full GC较为频繁可以先试着优化下JVM参数,把年轻代适当调大点,调大老年代满GC的比例,没有高并发可以不考虑CMS的并发清理失败不用调比例。
-Xms1536M -Xmx1536M -Xmn1024M -Xss256K -XX:SurvivorRatio=6 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=92 -XX:+UseCMSInitiatingOccupancyOnly
优化完发现没什么变化,Full GC的次数比Minor GC的次数还多了。
推测下Full GC比Minor GC还多的原因
1. 元空间不够导致的多余Full GC,可以看元空间使用。
2. 代码里头显示调用System.gc()造成多余的Full GC,这种一般线上尽量通过-XX:+DisableExplicitGC参数禁用,如果加上了这个JVM启动参数,那么代码中调用System.gc()就不会在GC。
3. 老年代空间分配担保机制。如果Minor GC之前因为机制先触发Full GC。后续如果触发一次Minor GC,然后可能老年代满又触发Full GC,但是这种情况不会OOM。
最快速度分析完这些我们推测的原因以及优化后,发现Young GC和Full GC依然很频繁了,而且看到有大量的对象频繁的被挪动到老年代。
1. 第一种方式查看对象占用总字节数和生产实例数
可以借助jmap命令大概看下是什么对象
jvisualvm启动—选择运行的进程—抽样器—内存(可以看实例数量),就是间隔调用jmp命令。
根据对象的字节总数和数量,查到了有大量User对象产生,这个可能是问题所在,但不确定,还必须找到对应的代码确认。
代码里全文搜索生成User对象的地方(适合只有少数几处地方的情况)。
第二种查看代码cpu占用时间
如果生成User对象的地方太多,无法定位具体代码,我们可以同时分析下占用cpu较高的线程,一般有大量对象不断产生,对应的方法代码肯定会被频繁调用,占用的cpu必然较高。
可以用上面讲过的jstack或jvisualvm—抽样器—cpu来定位cpu占用时间长的代码。
最终定位到的代码如下:
import java.util.ArrayList;
@RestController
public class IndexController {
@RequestMapping("/user/process")
public String processUserData() throws InterruptedException {
ArrayList<User> users = queryUsers();
for (User user: users) {
//TODO 业务处理
System.out.println("user:" + user.toString());
}
return "end";
}
/**
* 模拟批量查询用户场景
* @return
*/
private ArrayList<User> queryUsers() {
ArrayList<User> users = new ArrayList<>();
for (int i = 0; i < 5000; i++) {
users.add(new User(i,"zhuge"));
}
return users;
}
}
同时,java的代码也是需要优化的,一次查询出500M的对象出来,明显不合适,要根据之前说的各种原则尽量优化到合适的值,尽量消除这种朝生夕死的对象导致的Full GC。
5. 内存泄露问题
之前用了对象没有回收,占用内存,这是一种常见的内存泄漏。
一般电商架构可能会使用多级缓存架构,就是redis加上JVM级缓存,大多数同学可能为了图方便对于JVM级缓存就简单使用一个hashmap,于是不断往里面放缓存数据,但是很少考虑这个map的容量问题,结果这个缓存map越来越大,一直占用着老年代的很多空间,时间长了就会导致full gc非常频繁,这就是一种内存泄漏,对于一些老旧数据没有及时清理导致一直占用着宝贵的内存资源,时间长了除了导致Full GC,还有可能导致OOM。
这种情况完全可以考虑采用一些成熟的JVM级缓存框架来解决,比如ehcache等自带一些LRU数据淘汰算法的框架来作为JVM级的缓存,自动淘汰旧数据。