JVM 总结以及调优实战 -(6)JVM调优实战

24,476 阅读16分钟

对于需要进⾏JVM调优,或者遇到JVM相关问题,且不知如何去解决的同学,这篇⽂章真的⾮常值得阅读。

一:前言

JVM调优听起来很⾼⼤上,但是要认识到,JVM调优应该是Java性能优化的最后⼀颗⼦弹。

二:常⽤调优策略

这⾥还是要提⼀下,及时确定要进⾏JVM调优,也不要陷⼊“知⻅障”,进⾏分析之后,发现可以通过优化程序提升 性能,仍然⾸选优化程序。

2.1 选择合适的垃圾回收器:

  • CPU单核,那么毫⽆疑问Serial 垃圾收集器是你唯⼀的选择。
  • CPU多核,关注吞吐量 ,那么选择PS+PO组合。
  • CPU多核,关注⽤户停顿时间,JDK版本1.6或者1.7,那么选择CMS。
  • CPU多核,关注⽤户停顿时间,JDK1.8及以上,JVM可⽤内存6G以上,那么选择G1。

参数配置:

//设置Serial垃圾收集器(新⽣代) 
开启:-XX:+UseSerialGC 

//设置PS+PO,新⽣代使⽤功能Parallel Scavenge ⽼年代将会使⽤Parallel Old收集器 
开启 -XX:+UseParallelOldGC //CMS垃圾收集器(⽼年代) 
开启 -XX:+UseConcMarkSweepGC //设置G1垃圾收集器 开启 -XX:+UseG1GC

2.2:调整内存⼤⼩

现象:垃圾收集频率⾮常频繁。

原因:如果内存太⼩,就会导致频繁的需要进⾏垃圾收集才能释放出⾜够的空间来创建新的对象,所以增加堆内存 ⼤⼩的效果是⾮常显⽽易⻅的。

注意:如果垃圾收集次数⾮常频繁,但是每次能回收的对象⾮常少,那么这个时候并⾮内存太⼩,⽽可能是内存泄 露导致对象⽆法回收,从⽽造成频繁GC。

参数配置:

//设置堆初始值 
指令1:-Xms2g 
指令2:-XX:InitialHeapSize=2048m 

//设置堆区最⼤值 
指令1:`-Xmx2g` 
指令2: -XX:MaxHeapSize=2048m 

//新⽣代内存配置 
指令1:-Xmn512m 
指令2:-XX:MaxNewSize=512m

2.2 设置符合预期的停顿时间

现象:程序间接性的卡顿 原因:如果没有确切的停顿时间设定,垃圾收集器以吞吐量为主,那么垃圾收集时间就会不稳定。 注意:不要设置不切实际的停顿时间,单次时间越短也意味着需要更多的GC次数才能回收完原有数量的垃圾.

参数配置:

//GC停顿时间,垃圾收集器会尝试⽤各种⼿段达到这个时间
-XX:MaxGCPauseMillis

2.3 调整内存区域⼤⼩⽐率

现象:某⼀个区域的GC频繁,其他都正常。

原因:如果对应区域空间不⾜,导致需要频繁GC来释放空间,在JVM堆内存⽆法增加的情况下,可以调整对应区域 的⼤⼩⽐率。

注意:也许并⾮空间不⾜,⽽是因为内存泄造成内存⽆法回收。从⽽导致GC频繁。

参数配置:

//survivor区和Eden区⼤⼩⽐率 
指令:-XX:SurvivorRatio=6 
//S区和Eden区占新⽣代⽐率为1:6,两个S区2:6 
//新⽣代和⽼年代的占⽐ -XX:NewRatio=4 
//表示新⽣代:⽼年代 = 1:4 即⽼年代占整个堆的4/5;默认值=2

2.4 调整对象升⽼年代的年龄

现象:⽼年代频繁GC,每次回收的对象很多。

原因:如果升代年龄⼩,新⽣代的对象很快就进⼊⽼年代了,导致⽼年代对象变多,⽽这些对象其实在随后的很短 时间内就可以回收,这时候可以调整对象的升级代年龄,让对象不那么容易进⼊⽼年代解决⽼年代空间不⾜频繁 GC问题。

注意:增加了年龄之后,这些对象在新⽣代的时间会变⻓可能导致新⽣代的GC频率增加,并且频繁复制这些对象 新⽣的GC时间也可能变⻓。

配置参数:

//进⼊⽼年代最⼩的GC年龄,年轻代对象转换为⽼年代对象最⼩年龄值,默认值7 
-XX:InitialTenuringThreshol=7

2.5 调整⼤对象的标准

现象:⽼年代频繁GC,每次回收的对象很多,⽽且单个对象的体积都⽐较⼤。

原因:如果⼤量的⼤对象直接分配到⽼年代,导致⽼年代容易被填满⽽造成频繁GC,可设置对象直接进⼊⽼年代 的标准。

注意:这些⼤对象进⼊新⽣代后可能会使新⽣代的GC频率和时间增加。

配置参数:

//新⽣代可容纳的最⼤对象,⼤于则直接会分配到⽼年代,0代表没有限制。
-XX:PretenureSizeThreshold=1000000

2.6 调整GC的触发时机

现象:CMS,G1 经常 Full GC,程序卡顿严重。

原因:G1和CMS 部分GC阶段是并发进⾏的,业务线程和垃圾收集线程⼀起⼯作,也就说明垃圾收集的过程中业务 线程会⽣成新的对象,所以在GC的时候需要预留⼀部分内存空间来容纳新产⽣的对象,如果这个时候内存空间不 ⾜以容纳新产⽣的对象,那么JVM就会停⽌并发收集暂停所有业务线程(STW)来保证垃圾收集的正常运⾏。这个 时候可以调整GC触发的时机(⽐如在⽼年代占⽤60%就触发GC),这样就可以预留⾜够的空间来让业务线程创建 的对象有⾜够的空间分配。

注意:提早触发GC会增加⽼年代GC的频率。

配置参数:

//使⽤多少⽐例的⽼年代后开始CMS收集,默认是68%,如果频繁发⽣SerialOld卡顿,应该调⼩ 
-XX:CMSInitiatingOccupancyFraction 
//G1混合垃圾回收周期中要包括的旧区域设置占⽤率阈值。默认占⽤率为 65% 
-XX:G1MixedGCLiveThresholdPercent=65

2.7 调整 JVM本地内存⼤⼩

现象:GC的次数、时间和回收的对象都正常,堆内存空间充⾜,但是报OOM

原因: JVM除了堆内存之外还有⼀块堆外内存,这⽚内存也叫本地内存,可是这块内存区域不⾜了并不会主动触发 GC,只有在堆内存区域触发的时候顺带会把本地内存回收了,⽽⼀旦本地内存分配不⾜就会直接报OOM异常。

注意: 本地内存异常的时候除了上⾯的现象之外,异常信息可能是OutOfMemoryError:Direct buffer memory。 解决⽅式除了调整本地内存⼤⼩之外,也可以在出现此异常时进⾏捕获,⼿动触发 GC(System.gc())。

配置参数:

XX:MaxDirectMemorySize

三:JVM调优实战

以下是整理⾃⽹络的⼀些JVM调优实例:

3.1 ⽹站流量浏览量暴增后,⽹站反应⻚⾯响很慢

  • 1、问题推测:在测试环境测速度⽐较快,但是⼀到⽣产就变慢,所以推测可能是因为垃圾收集导致的业务线程停 顿。
  • 2、定位:为了确认推测的正确性,在线上通过jstat -gc 指令 看到JVM进⾏GC 次数频率⾮常⾼,GC所占⽤的时间 ⾮常⻓,所以基本推断就是因为GC频率⾮常⾼,所以导致业务线程经常停顿,从⽽造成⽹⻚反应很慢。
  • 3、解决⽅案:因为⽹⻚访问量很⾼,所以对象创建速度⾮常快,导致堆内存容易填满从⽽频繁GC,所以这⾥问题 在于新⽣代内存太⼩,所以这⾥可以增加JVM内存就⾏了,所以初步从原来的2G内存增加到16G内存。
  • 4、第⼆个问题:增加内存后的确平常的请求⽐较快了,但是⼜出现了另外⼀个问题,就是不定期的会间断性的卡 顿,⽽且单次卡顿的时间要⽐之前要⻓很多。
  • 5、问题推测:练习到是之前的优化加⼤了内存,所以推测可能是因为内存加⼤了,从⽽导致单次GC的时间变⻓从 ⽽导致间接性的卡顿。
  • 6、定位:还是通过jstat -gc 指令 查看到 的确FGC次数并不是很⾼,但是花费在FGC上的时间是⾮常⾼的,根据GC⽇ 志 查看到单次FGC的时间有达到⼏⼗秒的。
  • 7、解决⽅案: 因为JVM默认使⽤的是PS+PO的组合,PS+PO垃圾标记和收集阶段都是STW,所以内存加⼤了之 后,需要进⾏垃圾回收的时间就变⻓了,所以这⾥要想避免单次GC时间过⻓,所以需要更换并发类的收集器,因 为当前的JDK版本为1.7,所以最后选择CMS垃圾收集器,根据之前垃圾收集情况设置了⼀个预期的停顿的时间,上 线后⽹站再也没有了卡顿问题。

3.2 后台导出数据引发的OOM

问题描述:公司的后台系统,偶发性的引发OOM异常,堆内存溢出。

  • 1、因为是偶发性的,所以第⼀次简单的认为就是堆内存不⾜导致,所以单⽅⾯的加⼤了堆内存从4G调整到8G。
  • 2、但是问题依然没有解决,只能从堆内存信息下⼿,通过开启了-XX:+HeapDumpOnOutOfMemoryError参数 获 得堆内存的dump⽂件。
  • 3、VisualVM 对 堆dump⽂件进⾏分析,通过VisualVM查看到占⽤内存最⼤的对象是String对象,本来想跟踪着 String对象找到其引⽤的地⽅,但dump⽂件太⼤,跟踪进去的时候总是卡死,⽽String对象占⽤⽐较多也⽐较正 常,最开始也没有认定就是这⾥的问题,于是就从线程信息⾥⾯找突破点。
  • 4、通过线程进⾏分析,先找到了⼏个正在运⾏的业务线程,然后逐⼀跟进业务线程看了下代码,发现有个引起我 注意的⽅法,导出订单信息。
  • 5、因为订单信息导出这个⽅法可能会有⼏万的数据量,⾸先要从数据库⾥⾯查询出来订单信息,然后把订单信息 ⽣成excel,这个过程会产⽣⼤量的String对象。
  • 6、为了验证⾃⼰的猜想,于是准备登录后台去测试下,结果在测试的过程中发现到处订单的按钮前端居然没有做 点击后按钮置灰交互事件,结果按钮可以⼀直点,因为导出订单数据本来就⾮常慢,使⽤的⼈员可能发现点击后很 久后⻚⾯都没反应,结果就⼀直点,结果就⼤量的请求进⼊到后台,堆内存产⽣了⼤量的订单对象和EXCEL对象, ⽽且⽅法执⾏⾮常慢,导致这⼀段时间内这些对象都⽆法被回收,所以最终导致内存溢出。
  • 7、知道了问题就容易解决了,最终没有调整任何JVM参数,只是在前端的导出订单按钮上加上了置灰状态,等后 端响应之后按钮才可以进⾏点击,然后减少了查询订单信息的⾮必要字段来减少⽣成对象的体积,然后问题就解决 了。

3.3:单个缓存数据过⼤导致的系统CPU飚⾼

  • 1、系统发布后发现CPU⼀直飚⾼到600%,发现这个问题后⾸先要做的是定位到是哪个应⽤占⽤CPU⾼,通过top 找到了对应的⼀个java应⽤占⽤CPU资源600%。
  • 2、如果是应⽤的CPU飚⾼,那么基本上可以定位可能是锁资源竞争,或者是频繁GC造成的。
  • 3、所以准备⾸先从GC的情况排查,如果GC正常的话再从线程的⻆度排查,⾸先使⽤jstat -gc PID 指令打印出GC 的信息,结果得到得到的GC 统计信息有明显的异常,应⽤在运⾏了才⼏分钟的情况下GC的时间就占⽤了482秒, 那么问这很明显就是频繁GC导致的CPU飚⾼。
  • 4、定位到了是GC的问题,那么下⼀步就是找到频繁GC的原因了,所以可以从两⽅⾯定位了,可能是哪个地⽅频繁 创建对象,或者就是有内存泄露导致内存回收不掉。
  • 5、根据这个思路决定把堆内存信息dump下来看⼀下,使⽤jmap -dump 指令把堆内存信息dump下来(堆内存空 间⼤的慎⽤这个指令否则容易导致会影响应⽤,因为我们的堆内存空间才2G所以也就没考虑这个问题了)。
  • 6、把堆内存信息dump下来后,就使⽤visualVM进⾏离线分析了,⾸先从占⽤内存最多的对象中查找,结果排名 第三看到⼀个业务VO占⽤堆内存约10%的空间,很明显这个对象是有问题的。
  • 7、通过业务对象找到了对应的业务代码,通过代码的分析找到了⼀个可疑之处,这个业务对象是查看新闻资讯信 息⽣成的对象,由于想提升查询的效率,所以把新闻资讯保存到了redis缓存⾥⾯,每次调⽤资讯接⼝都是从缓存 ⾥⾯获取。
  • 8、把新闻保存到redis缓存⾥⾯这个⽅式是没有问题的,有问题的是新闻的50000多条数据都是保存在⼀个key⾥ ⾯,这样就导致每次调⽤查询新闻接⼝都会从redis⾥⾯把50000多条数据都拿出来,再做筛选分⻚拿出10条返回 给前端。50000多条数据也就意味着会产⽣50000多个对象,每个对象280个字节左右,50000个对象就有13.3M, 这就意味着只要查看⼀次新闻信息就会产⽣⾄少13.3M的对象,那么并发请求量只要到10,那么每秒钟都会产⽣ 133M的对象,⽽这种⼤对象会被直接分配到⽼年代,这样的话⼀个2G⼤⼩的⽼年代内存,只需要⼏秒就会塞满, 从⽽触发GC。
  • 9、知道了问题所在后那么就容易解决了,问题是因为单个缓存过⼤造成的,那么只需要把缓存减⼩就⾏了,这⾥ 只需要把缓存以⻚的粒度进⾏缓存就⾏了,每个key缓存10条作为返回给前端1⻚的数据,这样的话每次查询新闻信 息只会从缓存拿出10条数据,就避免了此问题的 产⽣。

3.4 CPU经常100% 问题定位

问题分析:CPU⾼⼀定是某个程序⻓期占⽤了CPU资源。

1、所以先需要找出那个进⾏占⽤CPU⾼。

top 列出系统各个进程的资源占⽤情况。

2、然后根据找到对应进⾏⾥哪个线程占⽤CPU⾼。

top -Hp 进程ID 列出对应进程⾥⾯的线程占⽤资源情况

3、找到对应线程ID后,再打印出对应线程的堆栈信息

printf "%x\n" PID 把线程ID转换为16进制。

jstack PID | gerp -A 100 [线程ID转换为16进制] 打印出进程的所有线程信息,从打印出来的线程信息中找到上⼀步转换为16进制的线程ID对应的线程信息。

4、最后根据线程的堆栈信息定位到具体业务⽅法,从代码逻辑中找到问题所在。

查看是否有线程⻓时间的watting 或blocked 如果线程⻓期处于watting状态下, 关注watting on xxxxxx,说明线程在等待这把锁,然后根据锁的地址找到持 有锁的线程。

3.5 内存飚⾼问题定位

分析: 内存飚⾼如果是发⽣在java进程上,⼀般是因为创建了⼤量对象所导致,持续飚⾼说明垃圾回收跟不上对象 创建的速度,或者内存泄露导致对象⽆法回收。

1、先观察垃圾回收的情况:

jstat -gc PID 1000 查看GC次数,时间等信息,每隔⼀秒打印⼀次。

jmap -histo PID | head -20 查看堆内存占⽤空间最⼤的前20个对象类型,可初步查看是哪个对象占⽤了内 存。

如果每次GC次数频繁,⽽且每次回收的内存空间也正常,那说明是因为对象创建速度快导致内存⼀直占⽤很⾼; 如果每次回收的内存⾮常少,那么很可能是因为内存泄露导致内存⼀直⽆法被回收。

2、导出堆内存⽂件快照,注意:live 参数含义

jmap -dump:live,format=b,file=/home/myheapdump.hprof PID dump堆内存信息到⽂件。

3、使⽤visualVM对dump⽂件进⾏离线分析,找到占⽤内存⾼的对象,再找到创建该对象的业务代码位置,从代码 和业务场景中定位具体问题。

3.6 数据分析平台系统频繁 Full GC

平台主要对⽤户在 App 中⾏为进⾏定时分析统计,并⽀持报表导出,使⽤ CMS GC 算法。

数据分析师在使⽤中发现系统⻚⾯打开经常卡顿,通过 jstat 命令发现系统每次 Young GC 后⼤约有 10% 的存活对 象进⼊⽼年代。

原来是因为 Survivor 区空间设置过⼩,每次 Young GC 后存活对象在 Survivor 区域放不下,提前进⼊⽼年代。

通过调⼤ Survivor 区,使得 Survivor 区可以容纳 Young GC 后存活对象,对象在 Survivor 区经历多次 Young GC 达到年龄阈值才进⼊⽼年代。

调整之后每次 Young GC 后进⼊⽼年代的存活对象稳定运⾏时仅⼏百 Kb,Full GC 频率⼤⼤降低。

3.6.1 业务对接⽹关 OOM

⽹关主要消费 Kafka 数据,进⾏数据处理计算然后转发到另外的 Kafka 队列,系统运⾏⼏个⼩时候出现 OOM,重 启系统⼏个⼩时之后⼜ OOM。

通过 jmap 导出堆内存,在 eclipse MAT ⼯具分析才找出原因:代码中将某个业务 Kafka 的 topic 数据进⾏⽇志异 步打印,该业务数据量较⼤,⼤量对象堆积在内存中等待被打印,导致 OOM。

3.6.2 鉴权系统频繁⻓时间 Full GC

系统对外提供各种账号鉴权服务,使⽤时发现系统经常服务不可⽤,通过 Zabbix 的监控平台监控发现系统频繁发 ⽣⻓时间 Full GC,且触发时⽼年代的堆内存通常并没有占满,发现原来是业务代码中调⽤了 System.gc()。

转载⽂章,讲解JVM的常⽤调优策略,以及⼀些⼯作中遇到的JVM调优实例。 这片文章并非原创,感觉原文贴近实战,特此留用,融入本系列中, 以上内容有参考: juejin.cn/post/694980… ,如果作者看到有不恰当或者侵权行为请联系本人。