本篇大纲
JVM调优策略JVM基本工具介绍JVM基本工具介绍jmapjmapjstackjstackjstatjstatJVM的优化思路JVM的优化思路JVM的优化思路JVM的优化思路JVM调优案例JVM调优案例调优案例调优案例JVM调优策略
- 第一阶段:JVM基本工具介绍的详细介绍
- 第二阶段:JVM的优化思路
- 第三阶段:JVM的真实调优案例
第一阶段:JVM基本工具介绍
jmap—>Java内存映像工具
jmapMemory Map for Java用于生成堆转储快照一般称为heapdump或dump文件。同时它还可以查询finalize执行队列、Java堆和方法区的 详细信息,如空间使用率、当前用的是哪种收集器等。
说简单点就是它能用来查看堆内存信息
jmap命令格式
jmap option vmid
option选项
jmap使用demo例子
例子:大家在随便创建一个spring-boot项目然后加上web模块启动就可以了,这里不展开详细的步骤
第一步:启动创建的spring-boot项目
第二步:在终端输入jps命令
jpsJava Virtual Machine Process Status Tool是Java提供的一个显示当前所有Java进程pid的命令
找到对应的Java进程号:49150
第三步:在终端根据jmap命令格式输入命令查看
第一个命令:jmap -histo 进程号
输入jmap -histo 49150来看下结果:
发现在终端显示不下,打印的东西太多,所以我们把打印的内容输出到文件中
输入jmap -histo 49150 > ./jmapLog.txt来看下结果:
我们发现在对应的文件路径下就生成了这么一个jmapLog.txt文件,打开来看下:
可以看到三块内容最左边的编号可以不管,从右往左看分别是
- class name:类名
- bytes:字节数
- instances:实例数
说白了就是打印每个类的实例数有多少,总共占用了多少字节或者说是内存大小
第二个命令:jmap -heap 进程号
根据上方的表格可知:这个命令是用来显示Java堆的详细信息,如使用哪种垃圾收集器、参数配置、分代状况等等,我们来打印一下看一下
输入jmap -heap 49150来看下结果:
大家如果和我有一样问题的,可以看下这篇文章,应该就能解决你的问题,没有当然最好:
使用 jmap 打印 Java 堆信息时报错:Can’t attach symbolicator to the process
经过上面文章中的调整,最终输入jhsdb jmap –heap –pid 49150命令后打印:
1 | java复制代码$ jhsdb jmap --heap --pid 49150 |
第一块内容:堆的配置信息
- MinHeapFreeRatio: 空闲堆空间的最小百分比,计算公式为:HeapFreeRatio =(CurrentFreeHeapSize/CurrentTotalHeapSize) * 100,值的区间为0到100,默认值为 40。如果HeapFreeRatio < MinHeapFreeRatio,则需要进行堆扩容,扩容的时机应该在每次垃圾回收之后。
- MaxHeapFreeRatio: 空闲堆空间的最大百分比,计算公式为:HeapFreeRatio =(CurrentFreeHeapSize/CurrentTotalHeapSize) * 100,值的区间为0到100,默认值为 70。如果HeapFreeRatio > MaxHeapFreeRatio,则需要进行堆缩容,缩容的时机应该在每次垃圾回收之后。
- MaxHeapSize: 最大堆内存
- NewSize: 新生代占用的空间
- MaxNewSize: 最大新生代可用空间
- OldSize: 老年代占用空间
- NewRatio: 新生代占的比例,2表示1:2,占三分之一
- SurvivorRatio: 两个Survivor区和eden区的比例,8表示2个Survivor:Eden = 2:8,意味着一个Survivor区占年轻代的1/10
- MetaspaceSize:元空间大小
- CompressedClassSpaceSize:指针压缩空间大小
- MaxMetaspaceSize: 最大元空间大小
- G1HeapRegionSize: G1垃圾收集器中的一个Region大小
第二块内容:正在使用的堆信息
因为我用的是jdk 11,默认使用是G1的垃圾收集器,所以打印的堆内存结构和jdk 8不同,大家根据自己真实的场景去分析,没必要和我保持一致
- G1 Heap:总的堆空间,regions就是region的个数,默认2048个,capacity是总的堆的大小,used是已经使用的堆空间,free是没有使用的堆空间,最后是使用比例
- G1 Young Generation:Young空间部分/年轻代大小,其中又分为Eden区和Survivor区,这里就不再详细介绍每块区域大小,大家直接看图就好
- G1 Old Generation:Old空间部分/老年代大小
第三个命令:jmap -dump 进程号
举个例子: 执行一下jmap -dump:format=b,file=jmapDump 49150
发现就会在对应的目录下生成名称为jmapDump的堆快照信息文件
设置自动下载
我们还可以设置自动下载,当内存溢出的时候自动dump文件
小贴士:内存很大的时候,可能会导不出来
- -XX:+HeapDumpOnOutOfMemoryError设置自动导出
- -XX:HeapDumpPath=./xxx/xxx/xxx路径
查看jmapDump例子
在test目录下新建测试类就是在死循环中创建对象,并且让这个对象一直被GC Roots引用,不会被回收
加上 -XX:+HeapDumpOnOutOfMemoryError、-XX:HeapDumpPath 这两个命令后运行,当内存溢出的时候自动dump
1 | java复制代码 |
我们在idea的配置中加上JVM参数,没有的同学按照下方红框框中选择JVM参数那一栏
-Xms10M -Xmx10M -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dumpTest.log
-Xms10M -Xmx10M是为了尽早发生OOM,配置好参数之后执行一下,发现产生了OOM
然后去搜这个dumpTest.log文件,发现也已经自动生成了,那么问题来了,我们怎么分析这份文件呢?
jvisualvm
jdk自带的这个工具通过导入就可以查看这个文件
小贴士:亲测,jdk11不支持这个工具,由于我电脑的jdk是双版本,所以这里演示的时候是切换到jdk8 然后打开的jvisualvm
我们在终端输入jvisualvm,打开工具后,通过文件导入打开下载下来的dumpTest.log文件
就可以看到显示的有基本信息、环境、系统属性和堆快照上的线程信息
我们切换到类的窗口上
可以知道哪几个类的实例最多,占用的空间大小、比例是多少,我们可以通过这样的方式,很清楚的知道堆中类的实例分布,如果发现某一个类的实例特别多,我们就可以去定位创建这个类实例的地方,进行排查,我们可以通过这个方法找出JVM内存飙升的原因
剩下的jmap命令,剩下的还有三个jmap命令,由于平常不常用,这里就不再演示了
jstack—>Java堆栈跟踪工具
jstackStack Trace for Java命令用于生成JVM当前时刻的线程快照
线程快照
线程快照就是当前JVM内每一条线程正在执行的方法堆栈的集合,生成线程快照的 目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等,都是导致线程长时间停顿的常见原因。
线程出现停顿时通过jstack来查看各个线程的调用堆栈, 就可以获知没有响应的线程到底在后台做些什么事情,或者等待着什么资源。
jstack命令格式
jstack option vmid
jstack例子
既然是用来定位线程出现长时间停顿的原因,如线程间死锁,那么我们就模拟一个线程的死锁,下面是一个简单的死锁类:
1 | java复制代码/** |
我们运行一下,发现程序一直卡在这里进行不下去
我们程序不终止,用jstack命令来看下怎么排查,首先通过jps命令找到java进程
然后用jstack 进程号来看下
1 | java复制代码2021-04-27 23:42:14 |
打印的都是进程中的线程信息,例如下面的这两个线程:
- Thread-1:线程名
- prio:优先级
- os_prio:操作系统级别的线程优先级
- tid:线程id
- nid:线程对应本地线程id
- java.lang.Thread.State:线程状态
继续往下看,到打印信息的尾端,我们发现jstack帮我们已经发现了一个死锁,说发现了一个Java层面的一个死锁
不仅如此,jstack还帮我们打印了死锁产生的原因,比如下方,由于Thread-1等待获取0x00007fc4758254a8这把锁,但是这把锁,现在被Thread-0持有,Thread-0等待获取0x00007fc475821408这把锁,但是这把锁,现在被Thread-1持有
打印了死锁产生的原因之外,还帮我们定位到了java的代码行数:
jvisualvm检测死锁
我们通过jvisualvm也可以快速的检测死锁,如下图所示
旁边的线程Dump就是执行我们上面说的jstack 进程id
关于jvisualvm你一定要知道的事
虽然jvisualvm能监控远程,但是一般是不用的,因为如果要监控服务器,那么服务器启动的时候要启动JMX的端口配置
但是一般生产环境中是不可能把这么重要的端口开放出去的,所以一般不使用jvisualvm监控线上的JVM,所以具体的配置方式就不再展开,但是开发环境、测试环境可以根据需要使用比如压测的时候
jstack找出占用CPU最高的线程
准备:我直接拿开发环境的来模拟演示了,大家看下演示过程
使用top命令找出cpu最高的进程
输入top查看cpu占用率最高的进程
根据图中显示,占用cpu最高的java进程是18955
使用top -p 进程号命令
使用top -p 进程号命令查看进程的内存使用情况,我们输入top -p 18955来看下
按H进入进程的详情
按H查看进程内每个线程的内存使用情况
最左边的PID就是我们的线程ID
根据jstack命令找到线程ID对应的代码
- 首先把线程ID转化成16进制的因为jstack信息里打印的线程ID都是16进制的,所以要把24091转换成16进制就是5e1b
- 执行jstack 18955|grep -A 10 5e1b,匹配这个线程所在行的后面10行代码,从堆栈中可以找到导致cpu飙升的调用方法
- 分析堆栈中打印的代码,如下图所示这个只是例子,别在意里面的内容
1 | java复制代码"http-nio-8080-ClientPoller" #43 daemon prio=5 os_prio=31 cpu=24.76ms elapsed=327.74s tid=0x00007fd3f33d0800 nid=0x5e1b runnable [0x000070000d58d000] |
jinfo—>Java配置信息工具
jinfoConfiguration Info for Java的作用是实时查看和调整JVM配置的各项参数
查看JVM参数
jinfo -flags 进程号
小贴士: jdk8的版本这个命令有问题和上面jmap命令失效的情况的一样,需要切换其他的jdk版本
查看系统参数
jinfo还可以使用-sysprops选项把虚拟机进程的System.getProperties()的内容打印出来,如下图所示,大家只要知道有这个命令就好,用的很少
1 | java复制代码VM Flags: |
重点:jstat—>JVM统计信息监视工具
jstatJVM Statistics Monitoring Tool是用于监视JVM各种运行状态信息的命令行工具这个命令很重要很重要。
它可以显示本地或者远程JVM进程中的类加载、内存、垃圾收集、即时编译等运行时数据,在没有GUI图形界面、只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的常用工具。
jstat命令格式
jstat [-命令选项] [vmid] [间隔时间][查询次数]
命令选项清单
第一个功能: 垃圾回收统计
jstat -gc 进程号
- S0C:当前survivor0区的大小
- S1C:当前survivor1区的大小
- S0U:survivor0区的已经使用大小
- S1U:survivor1区的已经使用大小
- EC:Eden区的大小
- EU:Eden区的使用大小
- OC:老年代的大小
- MC:元空间的大小
- MU:元空间的使用大小
- CCSC:指针压缩空间的大小
- CCSU:指针压缩空间的使用大小
- YGC:程序运行以来共发生Minor GC的次数
- YGCT:Minor GC消耗的时间根据案例可知:4次MinorGC总共花费0.021秒
- FGC:程序运行以来共发生Full GC的次数
- FGCT:总共Full GC消耗的时间
- CGC:G1并发收集Mixed GC次数
- CGCT:G1并发收集Mixed GC消耗的时间
- GCT:总共垃圾回收消耗的时候
第二个功能: 堆内存统计
jstat -gccapacity 进程号
- NGCMN:年轻代最小容量
- NGCMX:年轻代最大容量
- NGC:当前年轻代容量
- S0C、S1C、EC和上面GC信息中一样
- OGCMN:老年代最小容量
- OGCMX:老年代最大容量
- OGC:当前老年代大小
- OC:和上面GC信息中一样
- MCMN:元空间最小容量
- MCMX:元空间最大容量
- MC:当前元空间容量
- CCSMN:最小压缩指针空间大小
- CCSMX:最大压缩指针空间大小
- CCSC:当前压缩指针空间大小
- YGC、FGC、CGC和上面GC信息中一样
依次类推,如果想要查看专门的监视内容就下面这张图,注意点:不同垃圾收集器,打印出来的内容可能会不一样,这里就不一一带着大家去看了
JVM运行情况预估
我们还记得上面说的这个间隔时间吗,我们可以用这个间隔时间来预估JVM的运行情况
举个例子:想要每个1s执行一次jstat gc统计,总共统计10次,我们就可以这样执行jstat -gc 85164 1000 10,我们就能得到下方图中的统计信息本地随便启动了个Tomcat一直放在那里,所以没有发生变化,你们在写测试类的时候可以通过不断的new对象,来达到数据的变化监控
我们可以从这张图中看出哪些东西?
- 预测年轻代对象的增长速率
- Minor GC的触发频率和平均耗时平均耗时=YGCT/YGC,总的MinorGC耗时时间除MinorGC次数计算而出,得出系统间隔多久Minor GC频率会停顿多久Minor GC耗时
- 每次Minor GC之后有多少对象进入老年代,每次Minor GC之后观察EU、S0U、S1U和OU的变化情况,从而推断出每次Minor GC有多少对象进入老年代,再结合Minor GC频率判断老年代对象增长速率
- Full GC的触发频率和平均耗时FGCT/FGC
第二阶段: 常见的JVM优化思路
结合对象挪动到老年代的规则主要可以由以下优化思路:
- 简单来说就是尽量让每次Minor GC后的存活对象小于Survivor区的50%避免因为动态年龄判断机制而过早进入老年代,尽量都存活在年轻代中,尽量减少Full GC的频率,Full GC对JVM性能的影响很严重这种情况出现在高并发的系统,正常情况下每秒创建的对象都是很少的,但是某一时间段,并发量突然上升,导致新对象创建的过快,很容易因为动态年龄判断机制过早的进入老年代,所以这种情况下,要调整年轻代的大小,让这些对象都尽可能的留在年轻代,因为这些都是朝生夕死的对象
- 大对象需要大量连续内存空间的对象 比如字符串、数组要尽早进入老年代,因为年轻代用的是标记-复制算法,大对象在复制的时候会消耗大量的性能,所以要尽早的进入老年代使用-XX:PretenureSizeThreshold设置大小,超过这个大小的对象直接进入老年代,不会进入年轻代,这个参数只在Serial和ParNew两个GC收集器下有用
结合频繁发生Full GC的次数,主要由以下优化思路
- 除了正常的因为老年代空间不足而发生Full GC,还有老年代空间担保机制的存在,年轻代在每次Minor GC之前,JVM都会计算下老年代剩余可用空间,如果这个可用空间小于年轻代里现有的所有对象大小之和,就会看一个 -XX:-HandlePromotionFailure JDK 1.8默认设置参数是否设置,这个参数就是一个担保参数,担保一下如果老年代剩余空间小于历史每一次Minor GC后进入老年代的对象的平均值,就会先发生一次Full GC,再执行Minor GC,Minor GC后,老年代不够又会发生Full GC,这样一次完整的Minor GC就是两次Full GC,一次Minor GC
- 元空间不够会导致多余的Full GC,导致Full GC次数频繁
- 显示调用System.gc造成多余的Full GC,这种一般线上尽量通过 -XX:+DisableExplicitGC参数禁用,加上这个参数,System.gc就没有任何效果
第三阶段: JVM的调优案例
JVM的调优案例:线上发生频繁的Full GC
假设有这么一串JVM参数模拟真实场景下的JVM环境
1 | java复制代码-Xms1536M -Xmx1536M -Xmn512M -Xss256K -XX:SurvivorRatio=6 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly |
不清楚这些命令的同学请看下面的指令介绍,知道的可以跳过下面的这一截内容:
- -Xms1536M:设置JVM初始内存为1536M。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。
- -Xmx1536M:设置JVM最大可用内存为1536M
- -Xmn512M:设置年轻代大小为512M
- -Xss256K:设置每个线程的线程栈大小为256K
- -XX:SurvivorRatio=6:设置年轻代中Eden区与Survivor区的大小比值。设置为6,则两个Survivor区与一个Eden区的比值为2:6,一个Survivor区占整个年轻代的1/8
- -XX:MetaspaceSize=256M:设置元空间大小为256M
- -XX:MaxMetaspaceSize=256M:设置最大元空间大小为256M
- -XX:+UseParNewGC:设置年轻代垃圾回收器是ParNew
- -XX:+UseConcMarkSweepGC:设置老年代垃圾回收器是CMS
- -XX:CMSInitiatingOccupancyFraction=75:设置CMS在对老年代内存使用率达到75%的时候开始GC因为CMS会有浮动垃圾,所以一般都较早启动GC
- -XX:+UseCMSInitiatingOccupancyOnly:只用设置的回收阈值上面指定的75%,如果不指定,JVM仅在第一次使用设定值,后续则自动调整,一般和上一个命令组合使用
猜想一:由于动态年龄判断机制导致频繁的发生Full GC
那么由于动态年龄判断机制的原因导致的频繁发生Full GC,应该怎么调优呢,我们先从以下几个方面来看?
- 动态年龄判断机制的关键点在于年轻代的空间大小,所以首先就是要把年轻代的空间调大
- 如果是并发量大的系统,我们可以调小CMSInitiatingOccupancyFraction设定的值,避免产生Serial Old收集器的情况,但是如果是并发量小的系统,我们可以调大CMSInitiatingOccupancyFraction设定的值,充分利用堆空间
所以,经过调整之后,JVM的参数就如下图代码中所示:把年轻代的空间调大成1024M 这样老年代的空间就是512M 把CMS老年代内存使用阈值调大成90% 充分利用老年代的空间,如果并发量大的同学 有可能需要调低这个值 避免最后因为并发冲突导致使用Serial Old收集器
1 | java复制代码-Xms1536M -Xmx1536M -Xmn1024M -Xss256K -XX:SurvivorRatio=6 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=90 -XX:+UseCMSInitiatingOccupancyOnly |
猜想二:由于老年代空间担保机制导致频繁的发生Full GC
如果按照上面设置,把老年代设置小的话,很容易会因为老年代空间担保机制,导致频繁的发生Full GC,老年代空间担保机制的关键点在于每次Minor GC的时候进入老年代对象的平均大小,所以我们要控制每次Minor GC后进入老年代的对象平均大小
判断内存中对象的分布情况
使用jmap -histo 进程号的命令,观察内存中对象的分布情况,观察有没有比较集中的对象,因为如果是并发量高的系统,接口很有可能是集中的,创建的对象也是集中的,所以可以从cpu占用比较高的方法,也就是热点方法、内存占用比较多的对象这两个方面去分析
- 借助jvisualvm的抽样器寻找热点方法jdk8有 jdk11不支持
- 借助jmap -histo 进程号观察占用内存比较多的对象从你工程中的对象入手
优化方向
- 如果是循环创建对象的话,尽量控制循环次数比如每次查询5000条记录,这些记录如果加载到内存就是要创建不少的对象,如果这批对象经过Minor GC,很容易由于老年代空间分配担保机制,发生Full GC,所以要减少查询记录条数,从而减少创建的对象
- 最快也是最有效的办法,在预算允许的情况下,增加物理机器的配置,增大整个堆的内存,在条件允许的情况下,也不失为一个好方法
本文总结
好啦,以上就是这篇文章的全部内容,在这篇文章中,我们也经历了下面的几个步骤
- 第一阶段:讲述了jmap、jinfo、jstack、jstat各个命令的基本使用,以及有什么作用
- 第二阶段:讲述了JVM的优化思路方向,总共可以分为两个方向
- 第三阶段:根据线上频繁的发生Full GC这个案例讲述了各个命令是怎么配合使用的
命令有点多,大家可以多多收藏,等到下次遇到问题了,可以直接翻出来使用
絮叨
最后,如果感到文章有哪里困惑的,请第一时间留下评论,如果各位看官觉得小沙弥我有点东西的话 求点赞👍 求关注❤️ 求分享👥 对我来说真的 非常有用!!!
本文转载自: 掘金