JVM调优工具以及调优实战
# 调优工具以及调优实战
# JVM参数调优
java -XX:+UseContainerSupport -XX:MaxRAMPercentage=60.0 -XX:InitialRAMPercentage=60.0 -XX:MinRAMPercentage=60.0 -XX:NewRatio=2 -Xss512k -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=512M -Djava.awt.headless=true -d64 -server -Djava.awt.headless=true -Djava.net.preferIPv4Stack=true -Djavax.servlet.request.encoding=UTF-8 -Dfile.encoding=UTF-8 -XX:+AlwaysPreTouch -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/demo-api/logs/demo-api/ -jar demo-api-1.0.1-RELEASE.jar --server.port=8080 --management.server.port=8070 --apollo.meta=http://configserver:8080
-XX:+UseContainerSupport: 启用容器支持,允许JVM自动检测容器环境并调整内存使用以适应容器的限制。-XX:MaxRAMPercentage=60.0: 设置JVM可用内存的最大百分比为60%,限制JVM在物理内存中可以使用的最大比例。-XX:InitialRAMPercentage=60.0: 设置JVM初始内存的百分比为60%,用于指定JVM在启动时分配的内存比例。-XX:MinRAMPercentage=60.0: 设置JVM最小内存的百分比为60%,指定JVM在运行时所需的最小内存比例。-XX:NewRatio=2: 表示新生代和老年代的比例为 1:2,也就是新生代占整个堆内存的1/3,而老年代占2/3。-Xss512k: 设置线程栈的大小为512KB,影响每个线程的栈大小。-XX:MetaspaceSize=256M: 设置Metaspace的初始大小为256MB。-XX:MaxMetaspaceSize=512M: 设置Metaspace的最大大小为512MB。-Djava.awt.headless=true: 设置Java运行时为无头模式,用于在没有图形界面的环境中运行。-d64: 指示JVM在64位模式下运行。-server: 指示JVM以服务器模式运行,通常用于生产环境,以获得更好的性能。-Djava.net.preferIPv4Stack=true: 设置JVM偏好使用IPv4协议栈。-Djavax.servlet.request.encoding=UTF-8: 设置Servlet请求的编码为UTF-8。-Dfile.encoding=UTF-8: 设置文件编码为UTF-8。-XX:+AlwaysPreTouch: 指示JVM在启动时分配并预触摸所有堆内存,以确保内存已分配。-XX:+UseConcMarkSweepGC: 启用并发标记清除垃圾收集器(Concurrent Mark-Sweep GC)。-XX:CMSInitiatingOccupancyFraction=75: 设置CMS垃圾收集器的触发阈值为75%。-XX:+UseCMSInitiatingOccupancyOnly: 使用CMS的触发阈值来触发垃圾收集,而不使用其他条件。-XX:+HeapDumpOnOutOfMemoryError: 在发生内存溢出错误时生成堆转储文件。-XX:HeapDumpPath=/demo-api/logs/demo-api/: 指定堆转储文件的保存路径为/demo-api/logs/demo-api/。-jar demo-api-1.0.1-RELEASE.jar: 启动一个包含demo-api-1.0.1-RELEASE.jar的Java应用程序。--server.port=8080: 指定应用程序的HTTP端口为8080。--management.server.port=8070: 指定管理端口为8070。--apollo.meta=http://configserver:8080: 指定Apollo配置中心的元数据地址为http://configserver:8080。
参数调优
-XX:MetaspaceSize=200M -XX:MaxMetaspaceSize=400M -XX:+UseContainerSupport -XX:MaxRAMPercentage=50.0 -XX:InitialRAMPercentage=50.0 -XX:MinRAMPercentage=50.0 -Xss512k -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/
# JVM参数解析:
-XX:MetaspaceSize=200M: 设置Metaspace的初始大小为200MB。Metaspace是Java 8及更高版本中用于存储类元数据的区域。-XX:MaxMetaspaceSize=400M: 设置Metaspace的最大大小为400MB。这是Metaspace允许增长到的最大大小。-XX:+UseContainerSupport: 启用容器支持。这个选项允许JVM自动检测容器环境并调整内存使用,以便更好地适应容器的限制。-XX:MaxRAMPercentage=50.0: 设置JVM可用内存的最大百分比为50%。这个选项用于限制JVM在整个物理内存中可以使用的最大比例。-XX:InitialRAMPercentage=50.0: 设置JVM初始内存的百分比为50%。这个选项用于指定JVM在启动时分配的内存比例。-XX:MinRAMPercentage=50.0: 设置JVM最小内存的百分比为50%。这个选项用于指定JVM在运行时所需的最小内存比例。-Xss512k: 设置线程栈的大小为512KB。这影响每个线程的栈大小。-XX:+UseConcMarkSweepGC: 启用并发标记清除垃圾收集器(Concurrent Mark-Sweep GC)。这是一种用于管理Java堆内存中的垃圾的垃圾收集器。-XX:CMSInitiatingOccupancyFraction=75: 设置CMS(Concurrent Mark-Sweep)垃圾收集器的触发阈值为75%。当堆内存占用达到这个阈值时,CMS开始执行垃圾收集。-XX:+UseCMSInitiatingOccupancyOnly: 使用CMS的触发阈值来触发垃圾收集,而不使用其他条件。-XX:+HeapDumpOnOutOfMemoryError: 在发生内存溢出错误时生成堆转储(heap dump)文件。堆转储文件包含了内存中的对象快照,用于分析问题。-XX:+CMSParallelRemarkEnabled:并行执行CMS的remark阶段。-XX:+ScavengeBeforeFullGC:在执行Full GC之前先执行Minor GC。-XX:+CMSScavengeBeforeRemark:在CMS的remark阶段之前先执行Minor GC。-XX:ParallelCMSThreads=4:并行执行CMS的线程数设置为4。-XX:HeapDumpPath=/tmp/: 指定堆转储文件的保存路径为/tmp/。堆转储文件将保存在该目录下。
# JVM参数分析:
- Metaspace: 为Metaspace设置的初始大小为200M,最大大小为400M。这通常是足够的,除非有很多动态生成的类或使用了大量的第三方库。
- RAM使用: 设置了
UseContainerSupport,这意味着JVM会考虑容器的限制。为JVM堆设置的RAM百分比是50%,这意味着JVM会使用容器可用RAM的50%作为其最大堆大小。- 线程堆栈大小: 设置的线程堆栈大小为512k,这对大多数应用程序来说是足够的。
- GC: 您选择了
UseConcMarkSweepGC,这是一个并发标记-清除垃圾收集器,适用于响应时间要求严格的应用程序。- Heap Dump: 在OutOfMemoryError时,选择了生成堆转储,并将其保存在
/tmp/目录下。
# 输出GC日志
# 必备
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintTenuringDistribution
-XX:+PrintHeapAtGC
-XX:+PrintReferenceGC
-XX:+PrintGCApplicationStoppedTime
# 可选
-XX:+PrintSafepointStatistics
-XX:PrintSafepointStatisticsCount=1
# GC日志输出的文件路径
-Xloggc:/path/to/gc-%t.log
# 开启日志文件分割
-XX:+UseGCLogFileRotation
# 最多分割几个文件,超过之后从头文件开始写
-XX:NumberOfGCLogFiles=14
# 每个文件上限大小,超过就触发分割
-XX:GCLogFileSize=100M
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# jvm调优命令工具
# jps查看进程ID
# jmap
JVM Memory Map命令用于生成heap dump文件,还可以查询finalize执行队列、Java堆和永久代的详细信息,如当前使用率、当前使用的是哪种收集器等
jmap
pid 参数如下:
-heap:打印jvm heap的情况
-histo:打印jvm heap的直方图。其输出信息包括类名,对象数量,对象占用大小。
-histo:live :只打印存活对象的情况
-permstat:打印permanent generation heap情况
jmap -histo pid
- 查看内存信息,实例个数以及占用内存大小
jmap -heap pid
- 堆信息
jmap ‐dump:format=b,file=eureka.hprof 14660
- 堆内存dump
# jstack
用于查看线程状态
jstack pid
- jstack加进程id查找死锁
jstack找出占用cpu最高的线程堆栈信息
- 使用命令top -p <pid> ,显示你的java进程的内存情况,pid是你的java进程号,比如19663
- 按H,获取每个线程的内存情况
- 找到内存和cpu占用最高的线程tid,比如19664
- 转为十六进制得到 0x4cd0,此为线程id的十六进制表示
- 执行 jstack 19663|grep -A 10 4cd0,得到线程堆栈信息中 4cd0 这个线程所在行的后面10行,从堆栈中可以发现导致cpu飙高的调 用方法
- 查看对应的堆栈信息找出可能存在问题的代码
# jinfo
用来查看正在运行的java应用程序的扩展参数(JVM中-X标示的参数),甚至支持在运行时修改部分参数。
jinfo -flags pid
- 查看jvm的参数
jinfo -sysprops pid
- 查看jvm的参数
# jstat
jstat命令可以查看堆内存各部分的使用量,以及加载类的数量。 命令的格式如下: jstat [-命令选项] [vmid] [间隔时间(毫秒)] [查询次数]
jstat -gc pid
- 可以评估程序内存使用及GC压力整体情况
- S0C:第一个幸存区的大小,单位KB
- S1C:第二个幸存区的大小
- S0U:第一个幸存区的使用大小
- S1U:第二个幸存区的使用大小
- EC:伊甸园区的大小
- EU:伊甸园区的使用大小
- OC:老年代大小
- OU:老年代使用大小
- MC:方法区大小(元空间)
- MU:方法区使用大小
- CCSC:压缩类空间大小
- CCSU:压缩类空间使用大小
- YGC:年轻代垃圾回收次数
- YGCT:年轻代垃圾回收消耗时间,单位s
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间,单位s
- GCT:垃圾回收消耗总时间,单位s
jstat -gccapacity pid
- 堆内存统计
- NGCMN:新生代最小容量
- NGCMX:新生代最大容量
- NGC:当前新生代容量
- S0C:第一个幸存区大小
- S1C:第二个幸存区的大小
- EC:伊甸园区的大小
- OGCMN:老年代最小容量
- OGCMX:老年代最大容量
- OGC:当前老年代大小
- OC:当前老年代大小
- MCMN:最小元数据容量
- MCMX:最大元数据容量
- MC:当前元数据空间大小
- CCSMN:最小压缩类空间大小
- CCSMX:最大压缩类空间大小
- CCSC:当前压缩类空间大小
- YGC:年轻代gc次数
- FGC:老年代GC次数
jstat -gcnew pid
- 新生代垃圾回收统计
- S0C:第一个幸存区的大小
- S1C:第二个幸存区的大小
- S0U:第一个幸存区的使用大小
- S1U:第二个幸存区的使用大小
- TT:对象在新生代存活的次数
- MTT:对象在新生代存活的最大次数
- DSS:期望的幸存区大小
- EC:伊甸园区的大小
- EU:伊甸园区的使用大小
- YGC:年轻代垃圾回收次数
- YGCT:年轻代垃圾回收消耗时间
jstat -gcnewcapacity pid
- 新生代内存统计
- NGCMN:新生代最小容量
- NGCMX:新生代最大容量
- NGC:当前新生代容量
- S0CMX:最大幸存1区大小
- S0C:当前幸存1区大小
- S1CMX:最大幸存2区大小
- S1C:当前幸存2区大小
- ECMX:最大伊甸园区大小
- EC:当前伊甸园区大小
- YGC:年轻代垃圾回收次数
- FGC:老年代回收次数
jstat -gcold pid
- 老年代垃圾回收统计
- MC:方法区大小
- MU:方法区使用大小
- CCSC:压缩类空间大小
- CCSU:压缩类空间使用大小
- OC:老年代大小
- OU:老年代使用大小
- YGC:年轻代垃圾回收次数
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
jstat -gcoldcapacity pid
- 老年代内存统计
- OGCMN:老年代最小容量
- OGCMX:老年代最大容量
- OGC:当前老年代大小
- OC:老年代大小
- YGC:年轻代垃圾回收次数
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
jstat -gcmetacapacity pid
- 元数据空间统计
- MCMN:最小元数据容量
- MCMX:最大元数据容量
- MC:当前元数据空间大小
- CCSMN:最小压缩类空间大小
- CCSMX:最大压缩类空间大小
- CCSC:当前压缩类空间大小
- YGC:年轻代垃圾回收次数
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
- S0:幸存1区当前使用比例
- S1:幸存2区当前使用比例
- E:伊甸园区使用比例
- O:老年代使用比例
- M:元数据区使用比例
- CCS:压缩使用比例
- YGC:年轻代垃圾回收次数
- FGC:老年代垃圾回收次数
- FGCT:老年代垃圾回收消耗时间
- GCT:垃圾回收消耗总时间
# JVM运行情况预估
用 jstat gc -pid 命令可以计算出如下一些关键数据,有了这些数据就可以采用之前介绍过的优化思路,先给自己的系统设置一些初始性的JVM参数,比如堆内存大小,年轻代大小,Eden和Survivor的比例,老年代的大小,大对象的阈值,大龄对象进入老年代的阈值等。
# 年轻代对象增长的速率
可以执行命令 jstat -gc pid 1000 10 (每隔1秒执行1次命令,共执行10次),通过观察EU(eden区的使用)来估算每秒eden大概新增多少对象,如果系统负载不高,可以把频率1秒换成1分钟,甚至10分钟来观察整体情况。注意,一般系统可能有高峰期和日常期,所以需要在不同的时间分别估算不同情况下对象增长速率。
# Young GC的触发频率和每次耗时
知道年轻代对象增长速率我们就能推根据eden区的大小推算出Young GC大概多久触发一次,Young GC的平均耗时可以通过 YGCT/YGC公式算出,根据结果我们大概就能知道系统大概多久会因为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性能的影响。
# 系统频繁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参数设置如下:
‐Xms1536M ‐Xmx1536M ‐Xmn512M ‐Xss256K ‐XX:SurvivorRatio=6 ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M
‐XX:+UseParNewGC ‐XX:+UseConcMarkSweepGC ‐XX:CMSInitiatingOccupancyFraction=75 ‐XX:+UseCMSInitiatingOccupancyOnly
2
大家可以结合对象挪动到老年代那些规则推理下我们这个程序可能存在的一些问题
经过分析感觉可能会由于对象动态年龄判断机制导致full gc较为频繁
为了给大家看效果,我模拟了一个示例程序(见课程对应工程代码:jvm-full-gc),打印了jstat的结果如下:
jstat ‐gc 13456 2000 10000
对于对象动态年龄判断机制导致的full gc较为频繁可以先试着优化下JVM参数,把年轻代适当调大点:
‐Xms1536M ‐Xmx1536M ‐Xmn1024M ‐Xss256K ‐XX:SurvivorRatio=6 ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M
‐XX:+UseParNewGC ‐XX:+UseConcMarkSweepGC ‐XX:CMSInitiatingOccupancyFraction=92 ‐XX:+UseCMSInitiatingOccupancyOnly
2
优化完发现没什么变化,full gc的次数比minor gc的次数还多了

我们可以推测下full gc比minor gc还多的原因有哪些?
1、元空间不够导致的多余full gc
2、显示调用System.gc()造成多余的full gc,这种一般线上尽量通过XX:+DisableExplicitGC参数禁用,如果加上了这个JVM启动参数,那么代码中调用System.gc()没有任何效果
3、老年代空间分配担保机制
最快速度分析完这些我们推测的原因以及优化后,我们发现young gc和full gc依然很频繁了,而且看到有大量的对象频繁的被挪动到老年代,这种情况我们可以借助jmap命令大概看下是什么对象

查到了有大量User对象产生,这个可能是问题所在,但不确定,还必须找到对应的代码确认,如何去找对应的代码了?
1、代码里全文搜索生成User对象的地方(适合只有少数几处地方的情况)
2、如果生成User对象的地方太多,无法定位具体代码,我们可以同时分析下占用cpu较高的线程,一般有大量对象不断产生,对应的方法代码肯定会被频繁调用,占用的cpu必然较高
可以用上面讲过的jstack或jvisualvm来定位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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
同时,java的代码也是需要优化的,一次查询出500M的对象出来,明显不合适,要根据之前说的各种原则尽量优化到合适的值,尽量消除这种朝生夕死的对象导致的full gc
# 内存泄露到底是怎么回事
再给大家讲一种情况,一般电商架构可能会使用多级缓存架构,就是redis加上JVM级缓存,大多数同学可能为了图方便对于JVM级缓存就简单使用一个hashmap,于是不断往里面放缓存数据,但是很少考虑这个map的容量问题,结果这个缓存map越来越大,一直占用着老年代的很多空间,时间长了就会导致full gc非常频繁,这就是一种内存泄漏,对于一些老旧数据没有及时清理导致一直占用着宝贵的内存资源,时间长了除了导致full gc,还有可能导致OOM。
这种情况完全可以考虑采用一些成熟的JVM级缓存框架来解决,比如ehcache等自带一些LRU数据淘汰算法的框架来作为JVM级的缓存。