在生产系统中,高吞吐和低延迟一直都是 JVM 调优的最终目标,但这两者恰恰又是相悖的,鱼和熊掌不可兼得,所以在调优之前要清楚舍谁而取谁。一般计算任务和组件服务会偏向高吞吐,而 web 展示则偏向低延迟才会带来更好的用户体验。
本文从性能和经验上来分享一下 JVM 参数的设置。
调优之前可以先用 -XX:+PrintFlagsFinal 来查看虚拟机是否默认开启某参数,不同版本的 JDK 可能虚拟机默认开启的参数也略有不同,新学习一条神奇的参数的时候可以先去查找一下参数是否默认开启了。
$ 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
来关闭该项优化。