想写个一份百万QPS系统的JVM参数,感觉太标题党了,虽然这的确是,但还是朴实点;
JVM参数调优是性能重器,安全第一,不可乱用,更不能因为网上推荐文章(此篇)而随便用
之前关于JVM的几篇文章《是否需要主动GC》、《JIT优化》、《GC及JVM参数》;
这些都涉及到JVM参数,然道理懂不少,还是配置不好参数;调优的确是个费劲的事。这儿直接给一份参数,可以直接拿来主义,当然也有些参数需要配合硬件及应用环境,斟酌使用,一切以实战为准
其实有很多现成的,如elasticsearch、cassandra、VIP
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变动,最好还是明确设置
代码语言:javascript复制-XX: PrintFlagsInitial表示打印出所有XX选项的默认值
-XX: PrintFlagsFinal表示打印出XX选项在运行程序时生效的值
这些参数从功能大体分类一下
- 空间大小,类似-X参数,但这些空间各个JVM可能不同实现,如PermSize
- 监控类,帮助确定问题Trouble shooting Options
- 优化类,调优性能
内存类
-XX:PermSize=512m -XX:MaxPermSize=512m
代码语言:javascript复制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
有时查线上问题时,看到有异常信息,但只有
代码语言:javascript复制...
java.lang.NullPointerException
java.lang.NullPointerException
java.lang.NullPointerException
...
具体的异常栈没了,有时异常监控不位,人工发现这些异常时,却看不出哪里的问题,很是恼火
JVM对一些特定的异常类型做了Fast Throw优化,如果检测到在代码里某个位置连续多次抛出同一类型异常的话,C2会决定用Fast Throw方式来抛出异常,而异常Trace即详细的异常栈信息会被清空。这种异常抛出速度非常快,因为不需要在堆里分配内存,也不需要构造完整的异常栈信息
特定异常:
代码语言:javascript复制NullPointerException
ArithmeticException
ArrayIndexOutOfBoundsException
ArrayStoreException
ClassCastException
-XX: PrintGCCause
打印产生GC的原因,比如AllocationFailure什么的,在JDK8已默认打开,JDK7要显式打开一下
-XX: PrintGCApplicationStoppedTime
这是个非常非常重要的参数,但它的名字没起好,其实除了打印清晰的完整的GC停顿时间外,还可以打印其他的JVM停顿时间,比如取消偏向锁,class 被agent redefine,code deoptimization等等,有助于发现一些原来没想到的问题
代码语言:javascript复制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%的影响
看代码
代码语言:javascript复制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是其中的一项。但这个参数在关键系统里不建议打开
代码语言:javascript复制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
- 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的江湖偏方与真实效果