一般来说,每个公司对于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参数推荐
取消偏向锁: -XX:-UseBiasedLocking
加大Integer Cache: -XX:AutoBoxCacheMax=20000
启动时访问并置零内存页面: -XX:+AlwaysPreTouch
SecureRandom生成加速: -Djava.security.egd=file:/dev/./urandom
-XX:+PerfDisableSharedMem 原来JVM经常会默默的在*/tmp/hperf* 目录写上一点statistics数据,如果刚好遇到PageCache刷盘,把文件阻塞了,就不能结束这个Stop the World的安全点了
-XX:-UseCounterDecay 禁止JIT调用计数器衰减。默认情况下,每次GC时会对调用计数器进行砍半的操作,导致有些方法一直温热,永远都达不到触发C2编译的1万次的阀值。
-XX:-TieredCompilation 多层编译是JDK8后默认打开的比较骄傲的功能,先以C1静态编译,采样足够后C2编译。但我们实测,性能最终略降2%,可能是因为有些方法C1编译后C2不再编译了。应用启动时的偶发服务超时也多了,可能是忙于编译。所以我们将它禁止了,但记得打开前面的-XX:-UseCounterDecay,避免有些温热的方法永远都要解释执行。
为了稳健,还是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就足够了。
让full gc时使用CMS算法,不是全程停顿,必选。
但像R大说的,System GC是保护机制(如堆外内存满时清理它的堆内引用对象),禁了system.gc() 未必是好事,只要没用什么特别烂的类库,真有人调了总有调的原因,所以不应该加这个烂大街的参数。
运维有时会对启动参数做一些临时的更改,将每次启动的参数输出到stdout,将来有据可查。打印出来的是命令行里设置了的参数以及因为这些参数隐式影响的参数,比如开了CMS后, -XX:+UseParNewGC 也被自动打开。
为异常设置StackTrace是个昂贵的操作,所以当应用在相同地方抛出相同的异常N次(两万?)之后,JVM会对某些特定异常如NPE,数组越界等进行优化,不再带上异常栈。此时,你可能会看到日志里一条条 Nul Point Exception ,而之前输出完整栈的日志早被滚动到不知哪里去了,也就完全不知道这NPE发生在什么地方,欲哭无泪。 所以,将它禁止吧,ElasticSearch也这样干。
JVM crash时,hotspot 会生成一个error文件,提供JVM状态信息的细节。如前所述,将其输出到固定目录,避免到时会到处找这文件。文件名中的%p会被自动替换为应用的PID
-XX:ErrorFile=/var/log/hs_err_pid<pid>.log
复制代码
当然,更好的做法是生成coredump,从CoreDump能够转出Heap Dump 和 Thread Dump 还有crash的地方,非常实用。
在启动脚本里加上 ulimit -c unlimited 或其他的设置方式,如果有root权限,设一下输出目录更好
echo "/{MYLOGDIR}/coredump.%p" > /proc/sys/kernel/core_pattern
复制代码
什么?你不知道coredump有什么用?看来你是没遇过JVM Segment Fault的幸福人。
在Out Of Memory,JVM快死掉的时候,输出Heap Dump到指定文件。不然开发很多时候还真不知道怎么重现错误。
路径只指向目录,JVM会保持文件名的唯一性,叫java_pid${pid}.hprof。因为如果指向文件,而文件已存在,反而不能写入。
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOGDIR}/
复制代码
但在容器环境下,输出4G的HeapDump,在普通硬盘上会造成20秒以上的硬盘IO跑满,也是个十足的恶邻,影响了同一宿主机上所有其他的容器。