JVM调优之经验

在生产系统中,高吞吐和低延迟一直都是JVM调优的最终目标,但这两者恰恰又是相悖的,鱼和熊掌不可兼得,所以在调优之前要清楚舍谁而取谁。一般计算任务和组件服务会偏向高吞吐,而web展示则偏向低延迟才会带来更好的用户体验。

本文从性能和经验上来分享一下JVM参数的设置。

前言

调优之前可以先用-XX:+PrintFlagsFinal来查看虚拟机是否默认开启某参数,不同版本的JDK可能虚拟机默认开启的参数也略有不同,新学习一条神奇的参数的时候可以先去查找一下参数是否默认开启了。

1
$ java -server -XX:+PrintCommandLineFlags |grep XXXXXXX

也可以通过jinfo口令 jinfo -flags [pid]来查看

GC策略

目前来看还是CMS当道,吞吐率和响应时间阔以兼顾,G1嘛,鸡丸鸡丸,至今并没有展现出one的实力,不过据贵里某P8讲G1在大堆(20G+)下表现更突出,停顿会显著降低,可能之后随着高内存越来越经济和普及,G1才能名副其实的称为鸡one。

废话少说,

-XX:+UseConcMarkSweepGC

设置CMS做为垃圾收集,CMS开启后默认的新生代回收是ParNew,如果CMS出现“Concurrent Mode Failure”了还会启用Serial Old做备胎。


-XX:CMSInitiatingOccupancyFraction=75

默认值是68,这个可以根据实际调优目标来调整,这个参数就比较应开始提到的,调优目标是降低延迟还是提高吞吐,如果是为了降低单次GC延迟,那么这个值阔以再往低了调一些,不过调的太高可能导致老年代剩余空间不够招呼并发收集产生的浮动垃圾而频繁的触发Full GC。


-XX:+UseCMSInitiatingOccupancyOnly

使用CMS的话这个参数一定要加上,一定要加上,一定要加上,重要的事情说三遍,否则虚拟机后面还是会自作聪明的自己计算上个参数的比值。


-XX:MaxTenuringThreshold=5

默认15,这个值是设置新生代对象存活了多少次young GC后可以进入老年代,值设的高的话可以使老年代增长缓慢,但YGC的次数会明显增多,如果清楚YGC的执行频率和大多数对象的最长生命周期,这个值可以设低些,让那些对象早点进入老年代。
可以用-XX:+PrintTenuringDistribution来观察一段时间,然后调整合适的值。

ps:有一种野路子是此值设为0,新生代GC次数少,速度快,就是老年代GC会更加频繁一些,不过也最大利用了并发GC。不过我没在生产这么搞过,效果有待验证。


-XX:+ExplicitGCInvokesConcurrent

这个参数是用来代替,-XX:DisableExplicitGC的,NIO许多地方会显示的调用System.gc()来触发一次Full GC。许多时候别的地方优化一万点都赔不起这儿调上几次的。ExplicitGCInvokesConcurrent这个参数是配合CMS使用的,开启后System.gc()还是会触发Full GC,不过并不是一个完全的stop-the-world的Full GC,而是并发的CMS GC。


内存设置

现在线上业务系统基本物理内存都是够用的,不过物尽其用,我们调优就是争取让每M空间都发挥出最大的作用。内存的设置还是最直观见效的。

-Xmx500m ,-Xms500m

最大堆内存和最小堆内存,这两个值要设的一致,避免虚拟机还要动态的计算分配内存空间。
PS:堆也不是越大越好,大堆带来的后果就是单次GC会较长。


-Xmn250m

新生代大小,非G1收集器可以设置这个值,G1的官方建议是不要显示分配新生代和老年代空间大小,因为G1会通过网格化内存来动态分配new/old区,官方认为不设置new size是最佳实践。


-Xss2m

每个线程的栈空间大小,默认值是1m,一般不需要设置,除非有递归方法存在可能会爆栈。


-XX:PermSize=128m,-XX:MaxPermSize=256m

JDK8之前永久代的空间设置,Spring框架了大量依赖AOP的实现都用的动态代理生成字节码,所以设个最大值求保险。
不过JDK8之后取消了永久代,改为元空间(MetaSpace),这块属于本地内存,理论上可以利用系统剩余的所有内存,不过跑了多个实例的话还是要设置一下为妙:

-XX:MetaspaceSize=128m,-XX:MaxMetaspaceSize=256m


-XX:MaxDirectMemorySize=128m

这个属于对外内存,可以合理控制大小。Heap区总内存减去一个Survivor区的大小,不宜过大,否则可能heap size + Direct Memory Size把物理内存耗光。


-XX:SurvivorRatio=7

默认是8,新生代中Eden与Survivor的比值,过大的话可能Survivor存不下临时对象而频繁触发分配担保。可以根据GC日志看实际情况。


PS:
关于内存大小的设置完全要根据各个机器和应用自身的情况来设置。
可以通过jstat -gc [pid] 2000 30,每2s输出一次一共输出30次内存情况,看看各个区域增长的速度,最大空间等数据来修改内存设置。


监控输出

监控参数还是需要的,不然有时候线上偶尔OOM了真的不好重现。


-XX:+HeapDumpOnOutOfMemoryError,-XX:HeapDumpPath={path}

OOM的时候会输出dump快照到{path}目录,只需要指向目录,文件名JVM会保持唯一性。


-XX:+PrintGCDetails,-Xloggc:logs/gc.log,-XX:+PrintGCTimeStamps,-XX:+PrintGCDateStamps

打印GC详细记录,-XX:+PrintGC 这个口令是简单GC日志,为了更容易定位问题,我们开启Details模式,-Xloggc是把gc日志输出到指定文件。
-XX:+PrintGCTimeStamps显示的时间代表JVM启动至记录日志的时间。
-XX:+PrintGCDateStamps则会添加上每行信息的绝对日期。
其实开启了-Xloggc的话会隐式的开启-XX:+PrintGCTimeStamps,不过为了防止各版本JVM改动差异,还是显示的设置出来保险。


-XX:-OmitStackTraceInFastThrow

这是个比较容易被忽略的参数,而没有经验的话又往往很难定位到原因。
JDK5之后JVM对异常做了一个优化,对于一些频繁抛出的异常,JIT重新编译后会抛出没有堆栈信息的异常,-server模式下是默认开启的,因此在频繁抛出某个异常一段时间后,该优化开始起作用,即只抛出没有堆栈的异常信息。
但由于该优化是JIT编译后才启用的,所以开始该异常的抛出是有完整堆栈信息的,但运行一段时间可能发现没有任何堆栈信息,很难定位,初次遇到很容易摸不到头脑。
可以使用-XX:-OmitStackTraceInFastThrow来关闭该项优化。