概述
JVM的调优进行一下总结
JVM的调优核心是调GC. GC产品,堆代分布等等都是为了GC而调优的
JVM的调优依赖JVM运行过程的各种日志,这是判断调优点的基石
JVM的调优大致分为解决问题和优化调优
检测手段
# 套用网上一句话:不要相信任何参数调优的文章(包括本文)
# 打印最终参数 := 表示被修改后的
java -XX:+PrintFlagsFinal ..
# 打印初始参数
java -XX:PrintFlagsInitial ..
# 打印参数命令行
-XX:PrintCommandLineFlages
# 打印目标JVM进程参数
jinfo -flags <pid>
问题型
JVM的问题型调整主要是 FullGC频繁
FullGC频繁
元空间
元空间(或者说永生代)满是会产生FullGC的
确定元空间满的标准非常简单,查看各代分布就能发现元空间已经满了
jstat pid [-gccapacity 或者 -gcmetacapacity] 都可以
内存泄露
内存泄露,必然会引起频繁FullGC
内存泄露的一大标志是 回收量小,也就是每次回收后的前后差异非常低
内存泄露必然回收量小,但回收量小不代表必然内存泄露
所以如果发现回收量每次都很小,有必要下Dump用MAT检查下此时的内存对象
# 下载Dump下来分析
# 如果有内存泄露,这是必然能检测出来的方法
jmap -dump:[live,]format=b,file=xx.hprof <pid>`
# 快速简单识别
# 避免下Dump,但这个命令必带一次FullGC(先GC后打印)
jmap -histo[:live] <pid>
# 打印每个class的实例数目,占用内存大小,(*表示VM内部类)
# 如果某个对象实例占用内存呈现不正常的占比超大,就值得怀疑
#
# num #instances #bytes class name
# ----------------------------------------------
# 1: 65220 9755240 <constMethodKlass>
# 2: 65220 8880384 <methodKlass>
# 3: 11721 8252112 [B
# 4: 6300 6784040 <constantPoolKlass>
# 5: 75224 6218208 [C
# 6: 93969 5163280 <symbolKlass>
# 7: 6300 4854440 <instanceKlassKlass>
# 8: 5482 4203152 <constantPoolCacheKlass>
# 9: 72097 2307104 java.lang.String
# 10: 15102 2289912 [I
# 11: 4089 2227728 <methodDataKlass>
晋升失败
晋升失败是指年轻代如果分配不下,触发内存担保然后失败后产生的FullGC
晋升失败在不同的GC产品中又不同的表象
- CMS
promation failed - G1
to space exhausted
晋升失败有两种完全不同的解决方向,年轻代调整和老年代调整
如何抉择则取决于应用程序自身的情况
年轻代
调整年轻代是为了阻止错误晋升,也就是阻止每次都将大量的瞬时对象晋升老年代(这种情况一样会造成大量晋升失败),此时需要参考老年代回收量来辅助决策
如果老年代回收量每次都非常大(每次回收都能到一半甚至三分之二以上),就有相当大的几率判定是瞬时对象被错误晋升了(老年代的本意是长生命对象,也就是正常不应该有这么大的回收量,除非老年代里有相当大的瞬时对象进入了)
如果确定是错误晋升了,决定在年轻向调整又有两个解决方向,需参考老年代对象生命分布
- 扩Survivor区
如果老年代中的对象整体偏小,说明对象很多是非年龄满晋升,这说明S区小了(有年龄占比S区一半直接晋升的机制)
-xx:SurvivorRatio=N(8),往下压(默认8,说明S区默认年轻代的十分之一) - 扩年轻代,或提升晋升年龄,(或者加堆)
如果老年代中的对象整体偏大(或接近满年龄),说明Eden区小了(Eden区每满一次,复制整理一次年龄递增),年龄增长太快
-xx:NewSize=N扩充年轻代(相应Eden也会同步增大)
-xx:MaxTenuringThreshold=N(15,CMS:6)提升晋升年龄(大部分情况只需加大年轻代就可,除非单独调过)
-xx:TargetSurvivorRatio=N(50)直接晋升的判断百分比
调大年轻代,MiniorGC本身会高频触发,年轻代越大则代表需要扫描的对象越多,可能会有性能损失
但并不见得性能一定损失多少,其核心就在于扫描一次能回收多少对象(瞬时对象有多少)
MinorGC的性能损失主要是在内存整理而不是内存扫描上(MinorGC必带内存整理)
也就是说如果对象增多,但基本都被回收后需要加入内存整理(复制算法)的对象少则性能损失很小,但如果瞬时对象不多,大量对象都被滞留到年轻代然后不停的参与内存整理复制,就非常消耗性能了
老年代
如果对象本身没有被错误晋升,也就是说是老年代本身的问题,就需要真正从老年代着手了
老年代的调整目标只有一个,增大老年代分配担保的能力
- 加大老年代(或者加堆)
- 提前触发老年代GC,将MajorGC的触发阈值往下调
-xx:CMSInitiatingOccupancyFraction=N(80)CMS-GC的触发阈值
-xx:InitiatingHeapOccupancyPercent=N(45)G1-MixGC的触发阈值
老年代满
原理同,参看上节
特殊情况
并发标记失败
并发标记失败,是并发型GC(CMS或者G1)特有的问题
并发GC的特性是在只在标记GC-ROOT时STW.之后的标记过程中不STW(所以叫并发),也就是必有浮动垃圾存在(因为应用程序没有停止,所以会理所当然的继续产生新的垃圾)
并发标记失败是指,GC正在并发标记的时候,还未来得及回收之前老年代就满了,此时会直接停止并发标记,立即进入FullGC
并发标记失败非常好界定,因为此时GC会记录相应的异常信息
- CMS
concurrent mod failure - G1
concurrent-mod-abort
解决并发标记失败的核心思路是,必须保证在并发标记阶段不能让老年代用满
- 提前触发,让老年代有更多的空闲空间
CMS-XX:CMSInitiatingOccupancyFraction=N(80)下调
G1-xx:InitiatingHeapOccupancyPercent=N(45)下调 - 为老年代设置更大的预留空间(救助空间)
CMS内存碎片
这是一种CMS-GC的特有情况
CMS是一种标记清理,因为没有内存整理,所以必然会产生老年代内存碎片
这是CMS的最大劣势,也是一种无解的存在,只能在一定程度上缓解
-xx:+UseCMSCompactAtFullCollection 在每一次FullGC之后强制一次内存整理
G1大对象分配
这是一种G1的特有情况
每一个大对象都是直入老年代的,但G1的Region特性,使得在G1上有略微不同
G1的大对象分配必须找到连续的老年Region区来存放大对象(其它老年代本身是连续的),如果没有符合条件的连续老年代H区,就会触发FullGC
大对象分配失败也非常好界定,G1会记录相应的异常信息
280.008: [G1Ergonomics (Concurrent Cycles) request concurrent cycle initiation, reason: occupancy higher than threshold, occupancy: 62344134656 bytes, allocation request: 46137368 bytes, threshold: 42520176225 bytes (45.00 %), source: concurrent humongous allocation]
G1识别大对象的标准是:超过Region一半以上就视为大对象
解决办法是调大Region大小(将大对象的识别标准提高和减少Region的碎片化)
-xx:G1HeapRegionSize=N(1M)调大(上限32M)
调大Region对象是一种整体调整(不能针对某一个或某一类区),有一定的性能损耗
因为Region是G1的单次回收最小单位(G1依靠不停的回收一部分Region来获得停顿预测),调大Region则意味着粒度变大,单次回收的对象增多
优化型
MinorGC频繁
MinorGC主要是年轻代的瞬时对象回收,正常情况下,MinorGC必然是频次很高的
但MinorGC频繁有一个隐式问题是,对象的生命迭代非常快,可能产生的问题是将稍长一点的瞬时对象也推送到老年代,导致频繁的MajorGC
推定MinorGC是否正常(是否存在错误晋升)的参考指标是MajorGC的回收量以及老年代对象生命分布
详见上文
偏向锁停顿
这是使用synchronize锁的在高并发情况下的问题
synchronize锁有一个锁升级的膨胀优化过程(偏向锁->轻量锁->内核锁)
但是synchronize锁由偏向锁->轻量锁,也就是产生锁争夺的时候会产生线程停顿
- 挂起持有偏向锁的线程
- 锁状态由偏向错变为轻量锁,清理偏向指向
- 恢复持有偏向锁线程(但此时它已经失去了偏向身份)
如果打印安全点停顿日志,会发现有大量的偏向锁停顿日志
-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1
5.141: RevokeBias[13 0 2] [0 0 0 0 0] 0
Total time for which application threads were stopped: 0.0000782 seconds, Stopping threads took: 0.0000269 seconds
# – JVM启动之后所经历的毫秒数(上例中是5.141)
# – 触发这次暂停的操作名(RevokeBias)。
# 如果你看见”no vm operation”,就说明这是一个”保证安全点”。JVM默认每秒会触发一次安全点来处理那些非紧急的排队的操作。GuaranteedSafepointInterval选项可以用来调整这一行为(设置为0的话就会禁用该功能)
# – 停在安全点的线程的数量(13)
# – 在安全点开始时仍在运行的线程的数量(0)
# – 虚拟机操作开始执行前仍处于阻塞状态的线程的数量(2)
# – 到达安全点时的各个阶段以及执行操作所花的时间(0)
-XX:-UseBiasedLocking关闭偏向锁(直接是轻量锁开始)
是否关闭偏向锁取决于应用程序的实际场景(使用了synchronize的地方)
- 如果并发冲突非常激烈,偏向锁根本保持不住,就可以考虑关闭偏向锁
- 如果并发冲突不激烈,那么偏向锁是一种非常好的优化手段(偏向锁等于无锁)
提前占领内存页
-XX:+AlwaysPreTouch 启动时就分配好并置零所以内存页
- 提前分配内存页可以有效的降低启动后的第一次访问停顿
- 提前分配内存页会增大启动时间
这个一般用在防止启动风暴的解决方案里
在有启动风暴危险的系统中,必然会有一个预热过程,所以不在乎启动时间稍长(必然长),而是特别在乎启动之后的请求涌入(系统刚启动的时候是系统最脆弱的时候,热点编译,缓存,各种连接池往往还处在初始状态,所以必须要预热)
避免伸缩
以堆为例,堆分配时一般同时设置-Xmx=N -Xms=N
这是为了避免伸缩堆
如果设置最小100M,最大200M,那么在某次GC后占用降低,堆大小会从200M又被释放回收到100M
之后在使用过程中,又需要再次申请逐步扩充到200M,之后再次被释放,又需要再次申请
元空间上限
元空间(使用堆外内存),默认是无限的
对于程序而言,任何无限两个字都需要打上深深的问号
建议设置元空间上限(如果动态代理用得多就设大一点就行)
-xx:MetaspaceSize=512M或1G,2G -xx:MaxMetaspaceSize=N
并发收集线程数
并发收集线程数计算
# 如果CPU核数小于8核
线程数=CPU核数
# 如果CPU核数大于8核
线程数=8+(CPU核数-8)*(5/8)
# 需要注意的是 CMS需要再算一下
CMS的线程数= (上述线程数+3)/4
这在微小服务中可能是一个很可怕的数字
比如一个仅仅只是开一个线程扫描一个XXX然后发出通知的小作业,如果在一个64核的系统上
它的GC线程数是
` 8+(64-8)*5/8=43`
也就是它会开43个GC线程来处理GC(CMS需要12个线程),这个应用程序本身才跑了一个业务线程
-xx:ParallelGCThreads=N 手动设置线程数
GC调优案例
代理服务调优
代理服务是一个生产上的请求转发服务(简单来说就是接受一个请求,验证签名后请求转发到另一个系统去)
- 几乎都是瞬时对象(方法局部变量,接受请求处理然后转发,即宣告结束)
长生命对象几乎都是静态单例对象(比如配置) - 响应时间越短越好,不希望在这个转发过程产生太长停顿
- 一个业务占比非常非常小的微小服务,不希望侵占太多的系统资源
-Xmx=6g -Xms=6g # 堆6G
-xx:MetaspaceSize=512M -xx:MaxMetaspaceSize=512M # 元空间512M
-xx:+useConcMarkSweepGC # 使用CMS
-Xmn=2g # 年轻代2G (占比接近一半(理论上的最大占比))
-XX:SurvivorRatio=2 -XX:MaxTenuringThreshold=15
-xx:+UseCMSCompactAtFullCollection # 强制FullGC之后内存整理
-xx:ParallelGCThreads=4 # 压低GC线程数
核心思路是期望应用程序的GC全部依靠MinorGC解决
- 老年代4G并不是为了大量使用老年代,而是为了确保分配担保必然成功,转而使用MinorGC
- S区和年龄调大,是为了让对象尽可能就在年轻代解决
这里基于的认知是,一个对象最多在年轻代倒腾一到两次就会被回收(事实也的确如此,局部方法变量只有临界点才有可能活到第二次MinorGC,绝大部分对象在第一次MinorGC就会被回收了) - 线上是16核机器,该应用的理论GC线程数是10线程(CMS),压低到4
事实上用不到这么大的并发线程(极端一点说,用串行都不是不是),因为在这个应用中MajorGC是属于非常少的情况,而MinorGC不适用并发模式
Spark作业调优
Spark作业的特征是
- 大对象较多(序列化的字节数组)
- Spark作业对响应时间没有特别的大要求(正常停顿200MS是可以接受的)
Spark作业使用的G1
因为G1的自优化特性,需要干预G1的地方很少(也应该尽可能少干预G1工作过程)
# Spark作业的特殊性
# 堆大小 和 堆外内存设置 不在java参数里设置
-xx:+UseG1GC
-xx:G1HeapRegionSize=16M # 调大G1分区
-XX:MaxGCPauseMills=200ms # 期望停顿
# GC监控
-Xloggc:/tmp/spark.gc.log
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+G1SummarizeConcMark