JVM参数你设置对了吗?

前言

一般来说,每个公司对于JVM的参数都有规范的,甚至形成了一些公司层面的默认配置,如果遇到性能问题(比较特殊的使用场景),就会考虑从代码层次、JVM层次、甚至Linux服务器层次去进行优化。


堆设置

  • -Xms:初始堆大小

  • -Xmx:最大堆大小

  • -XX:NewSize=n:设置年轻代大小

  • -XX:NewRatio=n:设置年轻代和年老代的比值。如n=3,表示年轻代与年老代比值为1:3,年轻代占整个年轻和代年老代的1/4

  • -XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如n=3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5

  • -XX:MaxPermSize=n:设置持久代大小


收集器设置

  • -XX:+UseSerialGC:设置串行收集器

  • -XX:+UseParallelGC:设置并行收集器

  • -XX:+UseParalledlOldGC:设置并行年老代收集器

  • -XX:+UseConcMarkSweepGC:设置并发收集器


垃圾回收统计信息

  • -XX:+PrintGC

  • -XX:+PrintGCDetails

  • -XX:+PrintGCTimeStamps

  • -Xloggc:filename


并行收集器设置

  • -XX:ParallelGCThreads=n:设置并行收集器收集时使用的CPU数。并行收集线程数。

  • -XX:MaxGCPauseMillis=n:设置并行收集最大暂停时间

  • -XX:GCTimeRatio=n:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)并发收集器设置

  • -XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。

  • -XX:ParallelGCThreads=n:设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。


调优策略

年轻代设置

  • 响应时间优先的应用:尽可能设大,直到接近系统的最低响应时间限制(根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。
  • 吞吐量优先的应用:尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。

年老代设置

响应时间优先的应用:年老代使用并发收集器,所以其大小需要小心设置,一般要考虑和等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要减少年轻代和年老代花费的时间,一般会提高应用的效率

  • 并发垃圾收集信息
  • 持久代并发收集次数
  • 传统GC信息
  • 花在年轻代和年老代回收上的时间比例

吞吐量优先的应用:一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。

较小堆引起的碎片问题

  • XX:+UseCMSCompactAtFullCollection:使用并发收集器时,开启对年老代的压缩。

  • XX:CMSFullGCsBeforeCompaction=0:上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

官方指导

  • -Xms180m -Xmx180m 堆大小:设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍。

  • -XX:MetaspaceSize=64M -XX:MaxMetaspaceSize=64M 元空间:设置为老年代存活对象的1.2-1.5倍

  • -Xmn64m 年轻代:设置为老年代存活对象的1-1.5倍

  • 老年代:设置为老年代存活对象的2-3倍。

举例

 jstat -gc pid
  ###
   S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT
  13824.0 22528.0 13377.0  0.0   548864.0 535257.2  113152.0   46189.3   73984.0 71119.8 9728.0 9196.2     14    0.259   3      0.287    0.546

OU表示老年代所占用的内存为 47189.3 K(大约47M);那么jvm相应的配置参数应该做如下修改

-XX:MetaspaceSize=128M 
-XX:MaxMetaspaceSize=128M 
-Xms256m -Xmx256m

生产配置建议

关键系统的JVM参数推荐

  1. 取消偏向锁:-XX:-UseBiasedLocking
  2. 加大Integer Cache:-XX:AutoBoxCacheMax=20000
  3. 启动时访问并置零内存页面:-XX:+AlwaysPreTouch
  4. SecureRandom生成加速:-Djava.security.egd=file:/dev/./urandom

可选的性能参数

  1. -XX:+PerfDisableSharedMem 原来JVM经常会默默的在*/tmp/hperf* 目录写上一点statistics数据,如果刚好遇到PageCache刷盘,把文件阻塞了,就不能结束这个Stop the World的安全点了
  2. -XX:-UseCounterDecay 禁止JIT调用计数器衰减。默认情况下,每次GC时会对调用计数器进行砍半的操作,导致有些方法一直温热,永远都达不到触发C2编译的1万次的阀值。
  3. -XX:-TieredCompilation 多层编译是JDK8后默认打开的比较骄傲的功能,先以C1静态编译,采样足够后C2编译。但我们实测,性能最终略降2%,可能是因为有些方法C1编译后C2不再编译了。应用启动时的偶发服务超时也多了,可能是忙于编译。所以我们将它禁止了,但记得打开前面的-XX:-UseCounterDecay,避免有些温热的方法永远都要解释执行。
GC策略

为了稳健,还是8G以下的堆还是CMS好了,G1现在虽然是默认了,但其实在小堆里的表现也没有比CMS好,还是JDK11的ZGC引人期待。

-XX:+UseConcMarkSweepGC 
-XX:CMSInitiatingOccupancyFraction=75 
-XX:+UseCMSInitiatingOccupancyOnly

吞吐率优先的垃圾回收器

-XX:+UseParallelGC

晋升设置

-XX:MaxTenuringThreshold=2

这是改动效果最明显的一个参数了。对象在Survivor区最多熬过多少次Young GC后晋升到年老代,JDK8里CMS 默认是6,其他如G1是15。

Young GC是最大的应用停顿来源,而YGC后存活对象的多少又直接影响停顿的时间,所以如果清楚Young GC的执行频率和应用里大部分临时对象的最长生命周期,可以把它设的更短一点,让其实不是临时对象的新生代对象赶紧晋升到年老代,别呆着。

-XX:+PrintTenuringDistribution观察下,如果后面几代的大小总是差不多,证明过了某个年龄后的对象总能晋升到老生代,就可以把晋升阈值设小,比如JMeter里2就足够了。

-XX:+ExplicitGCInvokesConcurrent 但不要加-XX:+DisableExplicitGC

让full gc时使用CMS算法,不是全程停顿,必选。

但像R大说的,System GC是保护机制(如堆外内存满时清理它的堆内引用对象),禁了system.gc() 未必是好事,只要没用什么特别烂的类库,真有人调了总有调的原因,所以不应该加这个烂大街的参数。

监控配置

1. -XX:+PrintCommandLineFlags

运维有时会对启动参数做一些临时的更改,将每次启动的参数输出到stdout,将来有据可查。打印出来的是命令行里设置了的参数以及因为这些参数隐式影响的参数,比如开了CMS后,-XX:+UseParNewGC也被自动打开。

2. -XX:-OmitStackTraceInFastThrow

为异常设置StackTrace是个昂贵的操作,所以当应用在相同地方抛出相同的异常N次(两万?)之后,JVM会对某些特定异常如NPE,数组越界等进行优化,不再带上异常栈。此时,你可能会看到日志里一条条Nul Point Exception,而之前输出完整栈的日志早被滚动到不知哪里去了,也就完全不知道这NPE发生在什么地方,欲哭无泪。所以,将它禁止吧,ElasticSearch也这样干。

Crash 文件

1. -XX:ErrorFile

JVM crash时,hotspot 会生成一个error文件,提供JVM状态信息的细节。如前所述,将其输出到固定目录,避免到时会到处找这文件。文件名中的%p会被自动替换为应用的PID

-XX:ErrorFile=/var/log/hs_err_pid<pid>.log

2. coredump

当然,更好的做法是生成coredump,从CoreDump能够转出Heap Dump 和 Thread Dump 还有crash的地方,非常实用。

在启动脚本里加上 ulimit -c unlimited或其他的设置方式,如果有root权限,设一下输出目录更好

echo "/{MYLOGDIR}/coredump.%p" > /proc/sys/kernel/core_pattern

什么?你不知道coredump有什么用?看来你是没遇过JVM Segment Fault的幸福人。

3. -XX:+HeapDumpOnOutOfMemoryError(可选)

在Out Of Memory,JVM快死掉的时候,输出Heap Dump到指定文件。不然开发很多时候还真不知道怎么重现错误。

路径只指向目录,JVM会保持文件名的唯一性,叫java_pid${pid}.hprof。因为如果指向文件,而文件已存在,反而不能写入。

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOGDIR}/

但在容器环境下,输出4G的HeapDump,在普通硬盘上会造成20秒以上的硬盘IO跑满,也是个十足的恶邻,影响了同一宿主机上所有其他的容器。


可视化工具

最后推荐一些gc调优的可视化工具,

  • https://gceasy.io/,可以将gclog上传,并给出反馈意见

你可能感兴趣的:(调优实战,Java)