一份JVM参数

想写个一份百万QPS系统的JVM参数,感觉太标题党了,虽然这的确是,但还是朴实点;

JVM参数调优是性能重器,安全第一,不可乱用,更不能因为网上推荐文章(此篇)而随便用

之前关于JVM的几篇文章《是否需要主动GC》《JIT优化》《GC及JVM参数》

这些都涉及到JVM参数,然道理懂不少,还是配置不好参数;调优的确是个费劲的事。这儿直接给一份参数,可以直接拿来主义,当然也有些参数需要配合硬件及应用环境,斟酌使用,一切以实战为准

其实有很多现成的,如elasticsearchcassandraVIP

jvm参数总体分两种:标准参数(以-开始)与非标准参数;

非标准参数又分了两种:不太标准(以-X开始)与特别不标准(以-XX开始)

参数列表就标准到非标准一一进行说明

标准参数

顾名思义,标准参数中包括功能和输出的参数都是很稳定的,很可能在将来的JVM版本中不会改变。你可以用java命令(或者是用 java -help)检索出所有标准参数

-server

这个参数涉及分层编译策略,简单讲,就是把更多的代码更早地编译成本地代码

-D

应用附加配置参数,通过System.getProperty读取

-Djava.security.egd=file:/dev/./urandom

-Djava.net.preferIPv4Stack=true

-Djava.awt.headless=true

-Dspring.profiles.active=dev

非标准参数

非标准化的参数在将来的版本中可能会改变

在实际情况中X参数和XX参数并没有什么不同。X参数的功能是十分稳定的,然而很多XX参数仍在实验当中(主要是JVM的开发者用于debugging和调优JVM自身的实现)

X参数

-Xms2048m

-Xmx2048m

-Xmn2048m

-Xss512K

这几个老面孔,设置堆大小

Xms和Xmx设置一样,可以减轻伸缩堆大小带来的压力;Xmn新年代大小

Xss规定了每个线程堆栈的大小。一般情况下256K是足够了; 如果线程数较多,函数的递归较少,线程栈内存可以调小节约内存,默认1M

-Xloggc

-Xloggc:/dev/shm/gc.log

有人担心写GC日志会影响性能,但测试下来实在没什么影响,GC问题是Java里最常见的问题,没日志怎么行。

后来又发现如果遇上高IO的情况,GC时操作系统正在flush pageCache 到磁盘,也可能导致GC log文件被锁住,从而让GC结束不了。所以把它指向了/dev/shm 这种内存中文件系统,避免这种停顿,详见Eliminating Large JVM GC Pauses Caused by Background IO Traffic


XX参数

XX参数 虽然是最不稳定参数,但使用的最多,好神奇,很多特殊的性能调优都需要用到

  • 对于布尔类型的参数,我们有”+”或”-“,然后才设置JVM选项的实际名称。例如,-XX:+用于激活选项,而-XX:-用于注销选项
  • 对于需要非布尔值的参数,如string或者integer,我们先写参数的名称,后面加上”=”,最后赋值。例如, -XX:=给赋值

在使用此类参数时,可以使用两个命令先确认一下JVM默认值,防止JVM变动,最好还是明确设置

1
2
-XX:+PrintFlagsInitial表示打印出所有XX选项的默认值
-XX:+PrintFlagsFinal表示打印出XX选项在运行程序时生效的值

这些参数从功能大体分类一下

  1. 空间大小,类似-X参数,但这些空间各个JVM可能不同实现,如PermSize
  2. 监控类,帮助确定问题Trouble shooting Options
  3. 优化类,调优性能
  4. GC策略类

内存类

-XX:PermSize=512m -XX:MaxPermSize=512m

1
2
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option PermSize=200m; support was removed in 8.0
Java HotSpot(TM) 64-Bit Server VM warning: ignoring option MaxPermSize=256m; support was removed in 8.0

在JDK8之后,永久代向元空间的转换,配置项变成了

-XX:MetaspaceSize=200m -XX:MaxMetaspaceSize=256m

为什么需要这样转换?

1、字符串存在永久代中,容易出现性能问题和内存溢出。
2、类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
3、永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。
4、Oracle 可能会将HotSpot 与 JRockit 合二为一。

-XX:MaxDirectMemorySize=2048m

堆外内存的最大值,默认为Heap区总内存减去一个Survivor区的大小;像使用netty之类框架,用得多些。

在DirectByteBuffer中,首先向Bits类申请额度,Bits类有一个全局的 totalCapacity变量,记录着全部DirectByteBuffer的总大小,每次申请,都先看看是否超限 – 堆外内存的限额默认与堆内内存(由-XMX 设定)相仿,可用 -XX:MaxDirectMemorySize 重新设定。

如果已经超限,会主动执行Sytem.gc(),期待能主动回收一点堆外内存。然后休眠一百毫秒,看看totalCapacity降下来没有,如果内存还是不足,就抛出大家最头痛的OOM异常;详细可见《堆外内存》

-XX:ReservedCodeCacheSize=240M

JIT编译后二进制代码的存放区,满了之后就不再编译,对性能影响很大。JDK7默认不开多层编译48M,开了96M,而JDK8默认开多层编译240M。可以在JMX里看看CodeCache的大小,JDK7下的48M一般够了,也可以把它设大点,反正内存多

-XX:NewRatio=1

这个参数,与-Xmn or (-XX:NewSize and -XX:MaxNewSize) or -XX:NewRatio并列,都是设置年轻代大小。默认值为2, 也就是新生代占堆大小的1/3, 个人喜欢把对半分, 增大新生代的大小,能减少GC的频率(但也会加大每次GC的停顿时间),主要是看老生代里没多少长期对象的话,占2/3太多了。可以用-Xmn 直接赋值(等于-XX:NewSize and -XX:MaxNewSize同值的缩写),或把NewRatio设为1来对半分(但如果想设置新生代比老生代大就只能用-Xmn)

参数中带Ratio的还有一个-XX:SurvivorRatio,从字面意思看好像是新年代占比、survivor占比。其实是反的

-XX:NewRatio=4表示年老代与年轻代的比值为4:1

-XX:SurvivorRatio=8表示Eden区与Survivor区的大小比值是8:1:1, 因为Survivor区有两个

监控类

-XX:+PrintCommandLineFlags

让JVM打印出那些已经被用户或者JVM设置过的详细的XX参数的名称和值,还会打印出以及因为这些参数隐式影响的参数

打印出来,需要核实线上运行状态时,有据可查

-XX:-OmitStackTraceInFastThrow

有时查线上问题时,看到有异常信息,但只有

1
2
3
4
5
...
java.lang.NullPointerException
java.lang.NullPointerException
java.lang.NullPointerException
...

具体的异常栈没了,有时异常监控不位,人工发现这些异常时,却看不出哪里的问题,很是恼火

JVM对一些特定的异常类型做了Fast Throw优化,如果检测到在代码里某个位置连续多次抛出同一类型异常的话,C2会决定用Fast Throw方式来抛出异常,而异常Trace即详细的异常栈信息会被清空。这种异常抛出速度非常快,因为不需要在堆里分配内存,也不需要构造完整的异常栈信息

特定异常:

1
2
3
4
5
NullPointerException
ArithmeticException
ArrayIndexOutOfBoundsException
ArrayStoreException
ClassCastException

-XX:+PrintGCCause

打印产生GC的原因,比如AllocationFailure什么的,在JDK8已默认打开,JDK7要显式打开一下

-XX:+PrintGCApplicationStoppedTime

这是个非常非常重要的参数,但它的名字没起好,其实除了打印清晰的完整的GC停顿时间外,还可以打印其他的JVM停顿时间,比如取消偏向锁,class 被agent redefine,code deoptimization等等,有助于发现一些原来没想到的问题

1
2018-10-18T03:27:39.204+0800: 33729.026: Total time for which application threads were stopped: 0.0059280 seconds, Stopping threads took: 0.0001000 seconds

-XX:+PrintGCDateStamps

输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)

用PrintGCDateStamps而不是PrintGCTimeStamps,打印可读的日期而不是时间戳

-XX:+PrintGCDetails

输出GC的详细日志

-XX:ErrorFile

-XX:ErrorFile=${MYLOGDIR}/jvmerr_%p.log

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

-XX:+HeapDumpOnOutOfMemoryError

两个配合使用 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOGDIR}/

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

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

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


优化类

-XX:AutoBoxCacheMax

-XX:AutoBoxCacheMax=20000

这个参数的意义是缓存自动装箱最大值

设为20000后,我们应用的QPS有足足4%的影响

看代码

1
2
3
4
Integer i = 129;
Integer j = 129;

i == j //true or false ?

JDK默认只缓存 -128 ~ +127的Integer 和 Long,超出范围的数字就要即时构建新的Integer对象;

如果这儿配置了最大值20000,那就是[-128,20000]都不再创建新对象,但有点奇怪的时,你不能认为AutoBoxCacheMax的默认值是127

为什么配置值是20000呢,就得说到-XX:+AggressiveOpts参数,这是是一些还没默认打开的优化参数集合, -XX:AutoBoxCacheMax是其中的一项。但这个参数在关键系统里不建议打开

There’s a JVM option -XX:+AggressiveOpts that supposedly makes your JVM faster. Lots of people turn this on in Eclipse to try to make it faster. But it makes your JVM less correct. Today I found it to be the cause of a longstanding bug in dx.
http://code.google.com/p/android/issues/detail?id=5817

-XX:+AggressiveOpts was deprecated in JDK 11 and should be removed in JDK 12

1
2
3
4
5
-     bool AggressiveOpts                           := false           {product}
+ bool AggressiveOpts := true {product}

- intx AutoBoxCacheMax = 128 {C2 product}
+ intx AutoBoxCacheMax = 20000 {C2 product}

通过上面的打印设置配置值的参数,可以看出此项默认值是128,在打开AggressiveOpts参数时,是20000

-XX:-UseCounterDecay

禁止JIT调用计数器衰减。默认情况下,每次GC时会对调用计数器进行砍半的操作,导致有些方法一直温热,
永远都达不到触发C2编译的1万次(server默认值)的阀值,详细可参考《JIT优化》

-XX:-UseBiasedLocking

JDK1.6开始默认打开的偏向锁,会尝试把锁赋给第一个访问它的线程,取消同步块上的synchronized原语

如果始终只有一条线程在访问它,就成功略过同步操作以获得性能提升

为什么会有偏向锁出现?因为经验表明,其实大部分情况下,都会是同一个线程进入同一块同步代码块

但线上应用基本都是使用多线程,一旦出现锁竞争,就会锁膨胀,GC日志中有不少RevokeBiasd的纪录,像GC一样Stop The World的干活,虽然只是很短的停顿,但对于多线程并发的应用,取消掉它反而有性能的提升

-XX:+PerfDisableSharedMem

JVM经常会默默的在/tmp/hperf目录写上一点statistics数据,如果刚好遇到PageCache刷盘,把文件阻塞了,就不能结束这个Stop the World的安全点

禁止JVM写statistics数据的代价,是jps和jstat用不了;详细可看jstat的具体实现

-XX:MaxTenuringThreshold=4

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

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

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

-XX:+UnlockDiagnosticVMOptions -XX: ParGCCardsPerStrideChunk=1024

Linkined的黑科技, 上一个版本的文章不建议打开,后来发现有些场景的确能减少YGC时间,详见《难道这些 Java 大牛说的都是真的?》,简单说就是影响YGC时扫描老生代的时间,默认值256太小了,但32K也未必对,需要自己试验

-XX:+ExplicitGCInvokesConcurrent

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

-XX:+AlwaysPreTouch

启动时访问并置零内存页面;启动时就把参数里说好了的内存全部舔一遍,可能令得启动时慢上一点,但后面访问时会更流畅,比如页面会连续分配,比如不会在晋升新生代到老生代时才去访问页面使得GC停顿时间加长。ElasticSearch和Cassandra都打开了它


GC策略

配置-server时默认使用ParallelScavenge系的GC,是个吞吐量优先的收集器

虽然现在有了G1 GC,甚至JDK11后的ZGC,但在大型互联网项目上估计CMS还是主流

CMS三个基本配置

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

因为我们的监控系统会通过JMX监控内存达到90%的状况(留点处理的时间),所以设置让它75%就开始跑了,早点开始也能避免Full GC等意外情况(概念重申,这种主动的CMS GC,和JVM的老生代、永久代、堆外内存完全不能分配内存了而强制Full GC是不同的概念)。为了让这个设置生效,还要设置-XX:+UseCMSInitiatingOccupancyOnly,否则75只被用来做开始的参考值,后面还是JVM自己算

-XX:+ParallelRefProcEnabled -XX:+CMSParallelInitialMarkEnabled

并行的处理Reference对象,如WeakReference,默认为false,除非在GC log里出现Reference处理时间较长的日志,否则效果不会很明显,但我们总是要JVM尽量的并行,所以设了也就设了。同理还有-XX:+CMSParallelInitialMarkEnabled,JDK8已默认开启,但小版本比较低的JDK7甚至不支持

建议参数

-XX:ParallelGCThreads=? -XX:ConcGCThreads=?

-XX:ParallelGCThreads=n GC在并行处理阶段多少个线程,默认值和平台有关。(译者注:和程序一起跑的时候,使用多少个线程)

-XX:ConcGCThreads=n 并发收集的时候使用多少个线程,默认值和平台有关。(译者注:stop-the-world的时候,并发处理的时候使用多少个线程)

ParallelGCThreads=Processor < 8 ? 8 : 8 +( Processor - 8 ) ( 5/8 );

ConcGCThreads = (ParallelGCThreads + 3)/4

24个处理器,小于8个处理器时ParallelGCThreads按处理器数量,大于时按上述公式YGC线程数=18, CMS GC线程数=5。

CMS GC线程数的公式太怪,也有人提议简单改为YGC线程数的1/2。

一些不在乎停顿时间的后台辅助程序,比如日志收集的logstash,建议把它减少到2,避免在GC时突然占用太多CPU核,影响主应用。

而另一些并不独占服务器的应用,比如旁边跑着一堆sidecar的,也建议减少YGC线程数。

一个真实的案例,24核的服务器,默认18条YGC线程,但因为旁边有个繁忙的Service Mesh Proxy在跑着,这18条线程并不能100%的抢到CPU,出现了不合理的慢GC。把线程数降低到12条之后,YGC反而快了很多。 所以那些贪心的把YGC线程数=CPU 核数的,通常弄巧成拙。

不要-XX:+DisableExplicitGC

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

Reference

关键业务系统的JVM参数推荐

JVM源码分析之Jstat工具原理完全解读

《难道这些 Java 大牛说的都是真的?》

SecureRandom的江湖偏方与真实效果

公众号:码农戏码
欢迎关注微信公众号『码农戏码』