JVM调优思路

jstat -gc PID命令可以计算出一些关键数据,有了这些数据就可以采用一些优化思路,先给系统设置一些初始性的JVM参数,比如堆内存大小年轻代大小Eden和Survivor比例老年代大小大对象的阈值大龄对象进入老年代的阈值等。

优化思路其实简单来说就是尽量让每次Young GC后存活的对象小于Survivor区域的50%,都留存在年轻代里,尽量别让对象进入老年代。尽量减少Full GC的频率,避免频繁Full GCJVM性能的影响

JVM参数汇总查看命令

java -XX:+PrintFlagsInitial:打印出所有参数选项的默认值
java -XX:+PrintFlagsFinal:打印出所有参数选项在运行程序时生效的值

对象进入老年代的触发条件

  • Eden区没有足够空间进行分配,进行一次Minor GC,若survivor空间不足以放下剩余存活对象,则把新生代的对象提前转移到老年代中去,若老年代空间不足,则触发Full GC
  • 设置了大对象进入老年代的阈值若对象超过设置大小会直接进入老年代-XX:PretenureSizeThreshold单位字节,只在SerialParNew两个收集器下有效
  • 对象动态年龄判断,一批对象的总大小大于这块Survivor区域内存大小的50%,可以通过-XX:TargetSurvivorRatio参数指定,则此时大于等于这批对象年龄最大值的对象,直接进入老年代。对象动态年龄判断机制一般是在minor gc之后触发
  • 老年代空间分配担保机制,年轻代每次minor gc之前JVM都会计算老年代剩余可用空间,若可用空间小于年轻代里现有的所有对象大小之和,包括垃圾对象,就会检查-XX:-HandlePromotionFailure参数是否设置,jdk1.8默认设置,若已设置则检查老年代可用内存大小,判断是否大于之前每一次minor gc后进入老年代的对象的平均大小。若小于参数未设置,则触发一次Full gc,若回收完还是没有足够空间存放新的对象则发生OOM,若minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,也会触发full gcfull gc完之后如果还是没有空间放minor gc之后的存活对象,则也会发生OOM。

年轻代对象增长的速率

通过命令jstat -gc pid 1000 10隔1000毫秒执行1次命令共执行10次,通过观察EU即eden区的使用来估算每秒Eden大概新增多少对象,若系统负载不高,可把频率换成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计算得出。

Full GCMinor GC还多的情况

  • 元空间不够导致的多余full gc
  • 显示调用System.gc()造成多余的full gc,这种一般线上尽量通过-­XX:+DisableExplicitGC参数禁用,若加上该J参数,则代码中调用System.gc()没有任何效果
  • 老年代空间分配担保机制

频繁Full GC

频繁Full GC但是系统又没有有OOm的可能,根据上面的几种情况可能是触发了对象动态年龄判断机制或者是新生代的对象提前转移到老年代中或者有大量大对象的分配,导致老年代很快被占满从而频繁触发Full GC。

首先考虑的是把年轻代适当调大点,通过jstat -gc pid 300000 10命令查看频繁Full GC的情况是否有所好转,以及查看一下各各区域内存变化情况,从而进一步分析,若有大量对象频繁的被挪动到老年代,这种情况可以借助jmap命令大概看下是什么对象,从而排查具体的代码,若好转则再适当调整年轻代大小即可,若调大了反而情况恶化了,导致full gc的次数比minor gc的次数还多了

  • 首先可以通过jstat -gcmetacapacity 15184 5000 10命令监测元空间内存变化情况,确定是否是元空间不够导致。
  • 开启-­XX:+DisableExplicitGC参数禁用看是否还出现频繁GC
  • 检查是否如下图所示触发了老年代空间分派担保机制:

老年代空间分匹配担保机制

同时分析下占用cpu较高的线程,一般有大量对象不断产生,对应的方法代码肯定会被频繁调用,占用的cpu必然较高可以用jstackjvisualvm来定位cpu使用较高的代码。

内存泄露

一般电商架构可能会使用多级缓存架构,就是redis加上JVM级缓存,不断往JVM级缓存里面放缓存数据,可能漏考虑了容量问题,结果导致缓存越来越大,一直占用着老年代的很多空间,时间长了就会导致full gc非常频繁,这就是一种内存泄漏,对于一些老旧数据没有及时清理导致一直占用着宝贵的内存资源,时间长了除了导致full gc,还有可能导致OOM。

这种情况完全可以考虑采用一些成熟的JVM级缓存框架来解决,比如ehcache自带一些LRU数据淘汰算法的框架来作为JVM级的缓存

实际项目调优

对于系统调优可使用Skywalking链路追踪工具,查看请求处理在哪些地方比较慢,从而找到需要优化的点;对于网关Gateway底层是使用的Netty对CPU性能要求较高,若CPU性能不够可能导致由于网关的原因将系统的TPS拖下一大节,一般网关损耗在10%以内才是正常的;Gateway可调节Netty线程池的大小,可通过-Dreactor.netty.ioWorkerCount=16JVM参数设置工作线程数,默认为逻辑核心数,一般调大意义不是很大。

对于MySQL数据库也是比较吃CPU资源的,可通过Skywalking或通过Prometheus&Grafana中监控的MySQL指标确定是否存在慢查询或MySQL中开启慢查询查找慢查询SQL,对具体的SQL通过explain来分析是否走索引等情况具体进行优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- 查看是否开启慢查询
show variables like '%slow_query_log%';
-- 开启慢查询
set global slow_query_log = 1;
-- 查看慢查询阈值
show variables like '%long_query_time%';
-- 设置慢查询阈值,默认为1,单位s,设置完成需要重启session才生效
set global long_query_time = 0.5;
-- 查看慢查询是写文件还是写数据表,默认写文件
show variables like '%log_output%';
-- 设置慢查询写数据表
set global log_output = 'TABLE';
-- 默认OFF关闭,表是是否记录没有使用索引的查询
set global log_queries_not_using_indexes = ON;

-- 模拟慢查询
select sleep(12);
-- 查询慢查询数据表
select * from mysql.slow_log;
# 修改mysql重启后会恢复之前值,要彻底修改需要修改mysql的配置文件并重启
set global max_connections=500; # 默认151

需要特别说明的是对于一些离散度小的字段一般不建议建立索引,但若某个字段离散度小但是我们一般只使用其中一个两个值,且可以过滤掉百分之八九十的数据时,也可以建立索引,如delete_status字段,若0表示未删除,1表示删除,若存在大量删除数据,且删除数据基本不用的情况可给delete_status建立索引。

通过Arthasthread命令查看应用是否有很多占用CPU高且处于等待状态的线程,排查线程等待的原因,是否是应用中配置的数据库连接池连接数不够导致的等。

1
2
3
4
5
6
7
8
# Arthas中执行
thread -n 3 # 最繁忙的3个线程(占用cpu最多的前3个),输出栈信息
thread -b # 输出阻塞的线程栈信息,若响应慢,阻塞状态的线程比较多,需要重点关注

# JVM自带调优工具
jstat -gcutil PID 1000 1000 # 用jstat看下gc情况
jmap -histo PID | head -20 # 若FGC比较频繁可通过jmap查看下对象占用内存的情况
jinfo -flags PID # 当前堆内存情况

若内存不大,如4核8G的机器,可用默认的Parallel垃圾收集器,若对停顿时间有一定要求,JDK8版本可使用ParNew+CMS垃圾收集器组合,如下面配置,若是大内存的服务且对单机并发要求非常高,则一般可用G1垃圾收集器.

1
-Xms3072M -Xmx3072M -Xmn1536M -Xss1M  -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=6 -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFaction=92 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0

大多数时候系统瓶颈往往出现在数据库上,所以优化方案是尽可能的让各种操作被缓存以及其它各种中间件拦截,让尽量少请求的到达MySQL数据库。

在MySQL中尽量不要搞太多表关联的SQL查询,因为不好优化索引,所以建议对于一些多表的操作能用Java做的尽量用Java做,哪怕java实现可能费时间更多点,但是java应用扩容是很方便的,数据库扩容是比较麻烦。