JVM性能——垃圾回收器的优化策略
JDK版本:OpenJDK 1.8.0_352-b08
操作系统:CentOS 7
如果文章内出现测试数据测试代码:https://benchmarksgame-team.pages.debian.net/benchmarksgame/program/binarytrees-java-6.html
depth:23
关于JVM的
其他文章
基础调整
虽然不同回收器的收集方式不尽相同,但是在一些基础的配置上其实是一样的。
堆内存的优化
对GC优化最有效最基础的办法就是调整堆的大小。堆是否需要扩大、要扩多大是一个取舍的问题。如果堆太小,JVM会频繁的进行GC
我们可以给JVM设置一个非常大的堆,但是如果设置一个非常大的堆会面临三个问题:
- 需要占用更多的系统资源。
- 虽然GC频率降低了但是每次GC要扫描的内容变多了,每次GC的停顿会增加了。
- 可能遇见虚拟内存的问题。如果系统进行了内存交换,将内存不活跃的数据复制到磁盘中,这个时候JVM尝试进行GC的时候会生成很大的性能开销,这个时候GC的开销会有数量级的增长。
特别注意:调整堆大小永远不要超过机器的物理内存,如果存在多个JVM,则需要考虑所有JVM设置的总和。通常要给操作系统预留1G的空间
控制堆内存的参数
这些参数用来修改JVM堆的大小,给予应用太小的堆会频繁GC。
参数 | 说明 | 配置值 |
---|---|---|
-Xms | 堆的初始值 | -Xms512m:512m的初始堆;Xms1g:1G的初始堆, |
-Xmx | 堆的最大值 | -Xmx512m:512m的初始堆;Xmx1g:1G的初始堆, |
堆内存默认值
如果不设置JVM的堆内存,JVM会根据当前运行的环境设置默认的值
机器 | 初始值 | 最大值 |
---|---|---|
Linux | 512MB或者物理内存1/64中的最小值 | 32GB或者物理内存1/4中的最小值 |
macOS | 64MB | 1GB或者物理内存1/4中的最小值 |
32位Windows | 16MB | 256MB |
64位Windows | 64MB | 1GB或者物理内存1/4中的最小值 |
docker内存设置需要注意:在JDJ 8u192版本时如果使用默认配置,JVM不会根据容器的内存进行计算而是根据物理机的内存计算,这个问题在之后版本已经修复。
设置堆的最大值的经验
《Java性能指南》中给出的建议是。让其在Full GC后仍被占用30%。所以需要在程序运行至稳定状态后,并且已经创建了最大用户连接数,使用Jconsole连接程序强制执行Full GC。然后观察内存占用。然后确保其有额外0.5-1G的内存满足JVM的非堆需求
设置初始值和最大值相同
在每次回收后JVM会增加堆的大小来满足GC的性能目标,它会不断的增加堆的大小。但是如果你可以明确知道需要的堆的大小,如果设置初始值和最大值相同,这样每次回收的时候GC就不需要搞清楚是否需要
分代优化
对于绝大多数的GC来说,分代的设置也是通用优化方案
当堆大小被固定下来后,分代的优化就是下一个操作内容,如果将新生代设置的过大,YGC的频率会下降,但是YGC时间会变长,同时老年代会变小,会执行更加频繁的Full GC。但是新生代设置过小会导致YGC频率增加,进而导致更多对象晋升到老年代中。
控制分代的参数
参数 | 说明 |
---|---|
-XX:NewRatio=N | 新生代和老年代的比例 |
-XX:NewSize=N | 设置新生代初始值 |
-XX:MaxNewSize=N | 设置新生代最大值 |
-XmnN | 将新生代初始值和最大值设置为同一个值 |
新生代和老年代的比例
NewRatio默认值为2,计算新生代大小的公式是。
新生代初始大小=堆的初始大小/(1+NewRatio)
新生代最大大小=堆的最大大小/(1+NewRatio)
新生代的大小参数NewSize的优先级高于NewRatio,这个时候堆大小发生变化的时候,新生代的值将会根据NewSiz
e和MaxNewSize
值进行改变而不是根据上面的公式。
自适应大小
参数UseAdaptiveSizePolicy
用来开启JVM自适应调整堆的大小,这样很多时候我们不用担心设置过大的堆。JVM会考虑到GC算法的性能目标,自动优化堆和代的大小,保证内存分配达到最佳。这个参数是默认打开的,如果我们决定自己优化堆的大小时候可以关闭这个参数
-XX:-UseAdaptiveSizePolicy
如果将堆最大值和最小值设置成一样后,自适应参数实际上也会被关闭。
元空间优化
JVM加载类的元数据时候,会将这些数据设置在堆中单独的一个空间元空间
中。元空间中的信息只有在编译器和JVM运行时候时候用。元空间需要的大小和他需要加载的类数量成正比,因为元空间需要的大小取决于程序大小且默认的元空间没有最大值,所以一般很少去优化元空间。
启动优化
一般来说系统启动的时候会设置比较小的元空间,程序在启动的过程中会存在大量的Full GC,这是因为永久代在尝试调整元空间的大小,而启动服务需要的元空间是我们可以记录到的,所以这个时候可以根据程序需求给元空间设置一个合适的大小,这样可以加快服务的启动时间。
并行优化
目前除了Serial
回收器,所有的GC算法都是用了多线程,这样可以通过配置线程参数来优化GC。
-XX:ParallelGCThreads=N
控制 STW 并行时候的线程数,主要影响的范围是使用。
- 使用
-XX:+UserSerialGC
时的新生代和老年代回收线程 - 使用
-XX:UseG1GC
时的新生代回收线程 - 使用
-XX:+UseConcMarkSweepGC
时的收线程
默认情况下JVM会在每个CPU上运行一个线程,最多8个。超过8个后线程总数按照下面的公式
ParallelGCThreads = 8 + ((CPU数量-8) * 5/8)
这个时候是否决定调整CPU数量取决于服务器上CPU的资源,在有更多CPU且运行多个JVM服务的之后会存在过多的GC线程并行运行。这将导致相当多的竞争,这种情况可能需要我们手动限制回收器并行的线程数量。
GC优化
Parallel垃圾回收器
设置停顿目标
使用Parallel垃圾回收器的时候我们可以下面的设置来设置JVM的停顿目标
参数 | 说明 | 参数说明 |
---|---|---|
-XX:MaxGCPauseMillis | 期望每次垃圾回收的最长时间的毫秒数 | 整数,表示期待的毫秒数 |
-XX:GCTimeRatio | 期望应用在垃圾回收中的时间比例 | 大于0小于100的整数 |
MaxGCPauseMillis
如果可以我们期望能把这个参数设置的非常小,但实际上如果你设置的非常小,JVM为了满足性能需求会调整堆的大小,直到能够在指定时间内完成GC的大小,最终你会得到一个非常小的堆,然后导致JVM频繁的GC。所以具体设置的时间要在资源和业务中找到一个平衡值。如果设置过小的参数导致GC频率超过忍受范围,或者过小的堆导致业务无法进行都是不可行的。
下面两张图分别是设置MaxGCPauseMillis
为30和1000时候堆内存的大小
在30的时候最终新生代申请了619mb的内存
而在1000的时候获得了更多的堆内存使用
GCTimeRatio
Parallel
作为吞吐量收集器,很多时候使用吞吐量来调优是一个很好的方式,可能回降低部分程序响应速度,但是放在一个较长的时间范围,其能保证获得最高的效率。GCTimeRatio
的参数其实有点反直觉。它并不是直接表示期望的百分数,而是需要用下面的公式计算的
程序运行百分比=1-(1/(1+GCTimeRatio))
注意根据官方说明
The goal is specified by the command-line option -XX:GCTimeRatio=, which sets the ratio of garbage collection time to application time to 1 / (1 + )
描述说明可以通过这个参数计算垃圾收集时间占用应用程序时间的比例,公式为 : 1/(1+ GCTimeRatio的值)。这么看起来。GCTimeRatio应该理解为用户程序的运行时间与垃圾收集时间的比例,即
GCTimeRatio = userTime/GCTime
这个参数默认值为99。根据上述公式计算结果为0.99。也就是99%的时间用来进行应用程序。所以如果我们期望5%的时间用来进行GC,则需要将参数设置为19,而不是5。
一般来说我们不去设置MaxGCPauseMillis
的内容,而主要设置GCTimeRatio
。我们可以将GCTimeRatio
设置为3%-6%(《Java性能指南》推荐),然后给JVM一个较大的堆上限,在满足目标吞吐量的情况下JVM会为了达成预期目标而不断调整堆的大小,直到完成了吞吐量的目标和GCTimeRatio的预期。
CMS垃圾回收器
在新生代回收的时候,如果老年代没有足够的空间容纳晋升的对象的时候,这个时候会出现并发失败
,CMS会开始进行串行的Full GC。该操作是单线程的这将会导致服务出现较长的等待。
(concurrent mode failure): 1918089K->515102K(1926784K), 2.9751840 secs] 2071432K->515102K(2080128K), [Metaspace: 3177K->3177K(1056768K)], 2.9752628 secs] [Times: user=2.40 sys=0.56, real=2.97 secs]
比如这段并发失败的日志,GC用了接近3秒,此时所有服务都被暂停这对很多业务的影响是巨大的。
避免并发失败
并发失败的原因在于CMS清理老年代的速度不够快,当新生代晋升的时候老年代没有足够的空间。正常情况下,当老年代达到一个阈值后,并发周期就会开始,这个时候CMS开始扫描老年代的垃圾。CMS必须在剩余空间被填满前完成老年代的扫描和释放。既然如此我们的优化方式可以是。
- 更大的堆空间、更大的老年代
- 更频繁的后台GC
- 更多的GC线程
分代优化:这个在上面已经介绍了这里就不在说了。
调整CMS的执行频率
如果可以提前运行CMS的并发周期,则会给CMS充足的时间来进行老年代的回收,通过下面的参数可以调整CMS启动的频率
参数 | 说明 | 参数说明 |
---|---|---|
-XX:CMSInitiatingOccupancyFraction | 当老年代使用率到达指定数值后启动CMS的并发周期 | 大于0小于100的整数 |
-XX:+UseCMSInitiatingOccupancyOnly | JVM是否根据设置的规则开启并发周期 | 默认未设置,此时CMS并发周期启动时间根据自己的算法决定,如果期望按照自己设置启动,需要添加此配置 |
因为CMS并发周期的某些阶段依然会暂停服务,并且CMS并发周期需要抢占CPU资源,所以将CMSInitiatingOccupancyFraction设置的非常小并不是一个好的策略。所以最好事先监控应用运行平稳后老年代的数据比例,将CMSInitiatingOccupancyFraction设置的值要大于这个比例。
调整CMS后台线程
CMS使用ConcGCThreads
来控制GC的并发周期的线程数,其可以使用下面的公式计算
ConcGCThreads = (3 + ParallelGCThreads) / 4
不过需要注意的是此阶段是和应用线程并发,如果线程数过多会影响应用线程。
G1 垃圾回收器
和CMS一样,G1在并发周期也会遇见并发失败的问题,也是需要避免出现随之而来的Full GC。而G1的优化主要是在防止出现Full GC的努力上,JDK8中的Full GC是单线程的会产生比较大的停顿。
避免Full GC的方式
- 增加老年代,增加堆空间
- 增加后台线程数量
- 更加频繁的GC
- Mixed GC处理更多的内容
MaxGCPauseMillis
使用MaxGCPauseMillis是一个很好的选择,在G1为了实现MaxGCPauseMillis的目标会调整分代比例、堆大小,更频繁的GC以及尝试在Mixed GC中处理更多的数据。
当然针对上面的优化内容,我们也可以分别进行优化
后台线程
G1的并发标记周期是和应用程序在进行线程竞争。G1清理老年代的速度必须比对象晋升速度要快,G1也是使用-XX:ConcGCThreads
控制。此参数的默认值计算公式
ConcGCThreads = (2 + ParallelGCThreads) / 4
增加后台并行线程会让G1 GC在Mixed GC过程中在其他线程填满老年代之前,更快的完成老年代的回收工作。但是需要注意增加并发周期的线程也会和应用程序争抢CPU资源。
GC频率
如果GC 提前开始后台标记周期,则可以预留更多的时间和空间进行老年代的清理,可以使用这个参数控制。
XX:InitiatingHeapOccupancyPercent
这个参数表示当老年代占据堆的比例到达指定值后开始进行标记周期,默认值时45。此参数的设置需要考虑Full GC的频率和可用的资源,设置一个较小的值可以尽快开始并发标记周期,但是也会导致GC需要更多的CPU时间。而且因为G1的并发周期存在一些小停顿,所以会对应用线程产生影响。
下面图是设置InitiatingHeapOccupancyPercent
为70(70%回收)和30(30%回收)时的GC情况
在70的时候,因为老年代始终没有到达需要的比例所以一直没有进如并发周期
而在30%的时候就触发了
这里需要注意一个问题!!!
如果使用XX:G1NewSizePercent
将新生代比例设置好,比如40%。这个时候把XX:InitiatingHeapOccupancyPercent
设置的值非常大。这样永远触发不了并发周期进行mixed GC了,导致老年代回收不了。只有最后进入Full GC 才能回收Old区。
https://bugs.openjdk.org/browse/JDK-8142484
Mixed GC周期
在一个并发周期结束后,直到被标记的老年代区域完全被回收,G1才能进入下一个并发周期。所以可以让G1在一个并发周期中清除更多的区域。
Mixed GC要清除区域取决于三个因素。
- 扩大清理区域可以让Mixed GC每次回收更多资源。 可以通过下面参数控制。
-XX:G1MixedGCLiveThresholdPercent
此参数默认85。意思是如果一个Region中的存活对象低于Region大小的85%的话,就认定这个区域需要进行回收。
- Mixed GC周期总次数可以控制每次Mixed GC总体的耗时。 可以通过下面参数控制。
-XX:G1MixedGCCountTarget
此参数默认值为8。在混合回收阶段一次只回收一部分Region。然后恢复系统运行,再一段时间后继续暂停服务进行回收,重复8次。减少此参数则会加快最后混合回收的速度,但是每次回收停顿时间也会变长。增加这个参数会使得每次服务停顿保持在一个较低的水平。
- 为G1设置GC时间上限,会让G1更加频繁的进行YGC,而Mixed GC伴随YGC执行,所以也加快了Mixed GC的执行频率。可以通过下面参数控制。
-XX:MaxGCPauseMillis
虽然我们期望GC尽可能的小,但是实际上MaxGCPauseMillis设置的非常小并不是一个好的方式,过小的时间间隔会导致每次Mixed GC回收时间太短,回收的垃圾量太少。最后因为清理速度赶不上新增的速度,最后造成串行的Full GC。但是如果设置过大可能会导致一次全部并发标记后触发的Mixed GC次数变少,但每次的时间变长,STW时间变长,对应用的影响更加明显。