- 恶邻A君
唯品会在服务化体系改造的初期,一个对延时敏感的应用,偶然会发生一些超时,事发当时zabbix分钟级监控,dstat秒级监控的服务器指标都正常,应用,数据库,缓存,网络也正常,那这是为什么呢?
某天脑洞大开,把怀疑的目光投向了在后台运行日志收集程序Flume,发现它的GC运行得比较狂野,于是对它的GC线程数做了限制:
修改前:15分钟内, 大于30ms的业务调用173次, 大于50ms的23次
修改后: 246分钟内,大于30ms的业务调用41次, 大于50ms的4次
- 恶邻B君
又过了若干个月,又有某些应用,又开始抽风。这次相对好查一些,因为我们新升级了服务器的监控系统,只要在两台机器上做一下对比测试就好了。 只花了一个晚上,基本就能验明凶手了。
那这个新升级的监控系统,又是怎么影响到主应用的呢?找出它与应用有交互的部分,原来对于JVM的各种线程数信息,堆内存各代的信息,每拿一个数据都会启动一次JMX Client,所以每分钟都有一秒要连拿7个数据,启动7个JMX Client。
改进方法很简单,我们自己定制了一下JMX Client,将7个数据合并在一个命令里获得,另外定制了一下JMX Client的JVM参数,将它启动的动静尽量减少。
- 逆优化
可见,JVM是个运行服务端应用的好VM,但体量有点大。如果你只是想频繁地运行一段Java写的脚本,或者在跑一些辅助性的程序比如监控和日志收集,往常推荐的JVM参数也就不再合适里,需要进行逆优化才能做个安静的好邻居:
一、启动快速,动静小。
二、低成本,节约CPU、内存和线程。
三、低扰动,不干扰主应用的运行。
- 从失败的取经开始
第一时间,觉得和JDK自带的jmap,jstack们用一样的参数就好了,多简单。
在它们运行时,跑jps -v ,结果发现通通只有一个-Xms8m 。
还不死心,又去翻源码,JDK7在 Makefile.launcher,JDK8在CompileLaunchers.gmk,结果发现全部8M,通通8M,再没别的参数了。
有同学又从久远的记忆中想起一个-client,感觉也是比较弱气的选项,但在这个多核的64位Linux服务器上是根本无效的,一定是-server,必须是-server。
- 逆优化的思路
JVM与上述诉求相冲的几个地方:
各种吃内存
各种后台线程
JIT时CPU表现狂野
GC时CPU表现狂野
那我们就从这几个方面着手。
在开始折腾前,先准备好测试手段:
首先,给工具脚本配上GC 日志参数,在GC日志里就能看到实际启动参数,GC纪录,以及运行结束时内存各代的占用。
-Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime
其次,长期跑一个 pidstat -l 1| grep xxx ,紧密监控进程的CPU消耗。
最后,jstack看线程。
- 类的加载和编译优化
6.1 -Xverify:none
来自优化Eclipse启动速度的经验,说关闭Java类加载验证可以加快10% -15%的启动速度,嗯,好,加。
6.2 设定编译级别
JIT编译之后的代码比解释执行字节码更快,更省CPU。比如vjtools里的vjtop,没编译时每回运行要50%单核CPU,75ms执行完一个探测循环,而编译后10%单核,15ms就能完工。
但编译本身就需要CPU,也需要额外的编译线程。
如果脚本只简单的跑一次,比如vjtools里的vjmxcli,建议就不要进行JIT编译了,编译完了也用不上,直接解释执行就好。禁止它:-Djava.compiler=NONE
如果脚本用于密集计算,比如vjtools里的vjmap,则建议打开多层编译,一开始就对运行到的方法进行静态编译,不用等方法被调用1万次。多层编译在JDK8默认打开,显式打开:-XX:+TieredCompilation。
但打开多层编译也会导致程序运行初期有较多的编译任务,吃比较多的CPU,可以显式关掉多层编译 -XX:-TieredCompilation来对比一下,综合其带来的性能提升,脚本的运行时间的长短,以及额外的CPU支出来综合评价。
6.3 编译线程的设定
在24核服务器上,默认有4条C1编译线程,8条C2编译线程(多层编译下),可以把它设到最小的-XX:CICompilerCount=2。
6.4 未来黑科技-AOT
JIT真的不适合脚本,还是预先把代码编译(Ahead-of-Time,AOT) 更好。 JDK9里有一个Hotspot编译器组搞的试验性的jaotc,另一个选择是GraalVM全家桶里带的SubstrateVM,支持JDK8。
看各位大大炫,但我还没玩过。
- GC 优化
脚本们一般不介意GC延时,建议使用吞度量最的串行收集算法 -XX:+UseSerialGC,避免了其他GC算法所需的大量GC线程,更绝对保证了自己GC时不会影响到主应用。
如果依然想使用并行算法,就一定要设置GC线程数,在24核机器上YGC和CMS GC的线程数默认分别是18和5,为了避免成为恶邻A君。可设为:
-XX:ParallelGCThreads=4 -XX:ConcGCThreads=2
- 内存优化
首先,JVM的堆内存
一,默认的JVM初始内存大小,在大内存的服务器上会比较大,必须设置。
二,-Xms 与 -Xmx 不等时, 自动扩张并没有想象中那么智能和合理。
三、新生代默认只有1/3堆大小,而在脚本看来新生代才是大头。
建议根据GC日志的结果,完整设置-Xms 和 -Xmx,并用-Xmn(新生代占大头) 或-XX:NewRatio=1(一半半) 来设置新生代大小。
其次,每条线程的内存,从默认1M回到256k: -xss256k
其他永久代,CodeCache的初始值还算合理,没看到特别浪费的情况不用管。
- 小结
想起写这个,主要是看杨晓峰的《Java核心技术36讲》里的某几篇挑起的兴趣。