记一次线上FGC排查经历

背景:线上服务,启动后很快必定FGC一次,随后GC变正常。服务器上JDK版本为jdk1.8.0_66。启动参数为:

-Xms8g -Xmx8g -Xmn3g -Xss1024K -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC        -XX:+UseCMSCompactAtFullCollection -XX:SurvivorRatio=4 -XX:MaxTenuringThreshold=8                                                -XX:CMSInitiatingOccupancyFraction=80 -XX:+PrintClassHistogram -XX:+PrintGCDetails -XX:+PrintGCDateStamps        -XX:+PrintHeapAtGC -Xloggc:../log/gc.log

大概解释一下:初始堆8G,最大堆8G,年轻代3G,线程栈1024K,并行收集时GC线程数20,使用CMS收集器,年轻代使用ParNew收集器,每次FGC后整理老年代,eden区与survivor比例是4(即from:to:eden=1:1:4),晋升老年代复制次数为8(说明一下不是一定要复制8次才会晋升,当同一个年龄的对象总大小超过了survivor的一半也会晋升),老年代占用80%进行FGC,遇到Ctrl-Break后打印类实例的柱状信息(没试验过),打印GC详情,打印GC格式化的时间,打印GC前后堆空间信息,gc日志路径。

其中-XX:CMSInitiatingOccupancyFraction这个参数,很多都说需要保证一个公式:

(Xmx-Xmn)*XX:CMSInitiatingOccupancyFraction>Xmn*(1-1/(XX:SurvivorRatio+2))

意思就是老年代FGC前最小剩余的空间>eden+一个survivor的大小。原因是minor gc的时候,最坏情况如果所有年轻代对象都存活,都晋升到老年代,老年代需要有足够的连续空间来容纳这些对象。对于晋升担保的规则是:如果老年代剩余连续空间大于年轻代总大小或者历次晋升平均值,则进行minor gc,否则进行fgc后再minor gc。对于XX:CMSInitiatingOccupancyFraction参数是否必须遵守这个公式,我持保留意见。

启动后FGC的日志如下:

2018-11-29T18:29:10.202+0800: 1809.039: [GC (CMS Initial Mark) [1 CMS-initial-mark: 304647K(5242880K)] 1169342K(7864320K), 0.0175782 secs] [Times: user=0.30 sys=0.00, real=0.02 secs]
2018-11-29T18:29:10.220+0800: 1809.057: [CMS-concurrent-mark-start]
2018-11-29T18:29:10.239+0800: 1809.076: [CMS-concurrent-mark: 0.019/0.019 secs] [Times: user=0.10 sys=0.00, real=0.01 secs]
2018-11-29T18:29:10.239+0800: 1809.076: [CMS-concurrent-preclean-start]
2018-11-29T18:29:10.248+0800: 1809.085: [CMS-concurrent-preclean: 0.008/0.009 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
2018-11-29T18:29:10.248+0800: 1809.085: [CMS-concurrent-abortable-preclean-start]
 CMS: abort preclean due to time 2018-11-29T18:29:15.338+0800: 1814.174: [CMS-concurrent-abortable-preclean: 4.588/5.090 secs] [Times: user=4.78 sys=0.00, real=5.09 secs]
2018-11-29T18:29:15.339+0800: 1814.176: [GC (CMS Final Remark) [YG occupancy: 864695 K (2621440 K)]2018-11-29T18:29:15.339+0800: 1814.176: [Rescan (parallel) , 0.0186670 secs]2018-11-29T18:29:15.358+0800: 1814.195: [weak refs processing, 0.0000491 secs]2018-11-29T18:29:15.358+0800: 1814.195: [class unloading, 0.0072920 secs]2018-11-29T18:29:15.365+0800: 1814.202: [scrub symbol table, 0.0017787 secs]2018-11-29T18:29:15.367+0800: 1814.204: [scrub string table, 0.0005941 secs][1 CMS-remark: 304647K(5242880K)] 1169342K(7864320K), 0.0306670 secs] [Times: user=0.37 sys=0.00, real=0.03 secs]
2018-11-29T18:29:15.370+0800: 1814.207: [CMS-concurrent-sweep-start]
2018-11-29T18:29:15.375+0800: 1814.211: [CMS-concurrent-sweep: 0.004/0.004 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
2018-11-29T18:29:15.375+0800: 1814.211: [CMS-concurrent-reset-start]
2018-11-29T18:29:15.403+0800: 1814.240: [CMS-concurrent-reset: 0.028/0.028 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]

解决过程:分析GC日志

第一行初始标记阶段,可以发现堆空间只占用304647K,总共堆空间大小5242880K大小,所以完全没有达到FGC的80%条件。

那么导致FGC的原因还有担保失败,老年代剩余空间大于年轻代大小,所以也不是这个原因。

而且回收后老年代和年轻代空间几乎没有变化,那么说明不是它们引起的。因为是JDK1.8,所以矛头指向了jdk1.8新的一个空间metaspace。从头开始分析GC日志,这次只关注metaspace的变化。

Metaspace       used 16518K, capacity 16734K, committed 17152K, reserved 1064960K

Metaspace       used 19805K, capacity 20194K, committed 20480K, reserved 1067008K

Metaspace       used 20561K, capacity 20996K, committed 21296K, reserved 1069056K

Metaspace       used 20710K, capacity 21200K, committed 21552K, reserved 1069056K

可以发现,metaspace一直再增加,倒数第二行为FGC前,最后一行为FGC后。那么metaspace是不是导致FGC的罪魁祸首呢?我们来分析一下,因为启动参数没有任何关于metaspace的设置,所以我们查看一下metaspace的默认值,使用命令:java -XX:+PrintFlagsInitial|grep Meta查看

uintx InitialBootClassLoaderMetaspaceSize       = 4194304                             {product}
    uintx MaxMetaspaceExpansion                     = 5452592                             {product}
    uintx MaxMetaspaceFreeRatio                     = 70                                  {product}
    uintx MaxMetaspaceSize                          = 18446744073709551615                    {product}
    uintx MetaspaceSize                             = 21810376                            {pd product}
    uintx MinMetaspaceExpansion                     = 340784                              {product}
    uintx MinMetaspaceFreeRatio                     = 40                                  {product}
     bool TraceMetadataHumongousAllocation          = false                               {product}
     bool UseLargePagesInMetaspace                  = false                               {product}

其中MetaspaceSize大小大概为20.799M代表metaspace到达这个值后会进行GC,我们可以比较一下gc前metaspace的大小是21296Kb,大概为20.796M,gc后metaspace的大小是21552KB,大概为21.046MB,所以确实是metaspace的大小达到了第一次FGC的阈值。再观察一下已经启动很久的服务,发现metaspace大概占用空间22M左右,我们调整一下metaspace进行GC的阈值-XX:MetaspaceSize=64M,启动后发现不会在很快出现FGC。

完美解决,撒花。

这里再总结一下metaspace。

metaspace分为了两部分内容:klass space和noklass space。

klass space保存的都是class文件在jvm的数据结构,是有-XX:CompressedClassSpaceSize参数来控制,默认大小:

 uintx CompressedClassSpaceSize                  = 1073741824                          {product}

noklass space保存的都是非klass的内容,比如常量池保存在这里,有参数-XX:InitialBootClassLoaderMetaspaceSize来控制,默认大小:

uintx InitialBootClassLoaderMetaspaceSize       = 4194304                             {product}

有关metaspace的参数依次解释一下:

MetaspaceSize仅仅表示metaspace第一次触发gc的阈值,这个值并不代表初始分配metaspace的大小,所以设置大了不必担心空间浪费。一般我会把这个值设置大于服务稳定后metaspace的大小,防止出现gc的情况。metaspace的gc阈值会一直改变的,和expansion两个参数相关,后面解释。

where MetaspaceSize is the initial amount of space(the initial
high-water-mark) allocated for class metadata (in bytes) that may induce a
garbage collection to unload classes. The amount is approximate. After the
high-water-mark is first reached, the next high-water-mark is managed by
the garbage collector

MaxMetaspaceSize表示metaspace的最大空间大小,可以看到默认非常大,一般也需要设置一下,防止代码bug导致metaspace无限变大。

where MaxMetaspaceSize is the maximum amount of space to be allocated for class
metadata (in bytes). This flag can be used to limit the amount of space
allocated for class metadata. This value is approximate. By default there
is no limit set.

MinMetaspaceFreeRatio:目的是减少gc触发的概率。

先解释一下gc阈值扩大规则,当达到的第一次gc阈值,即MetaspaceSize之后,会对触发gc的阈值进行扩大,不足MinMetaspaceExpansion时扩大MinMetaspaceExpansion,超过MinMetaspaceExpansion不足MaxMetaspaceExpansion时扩大MaxMetaspaceExpansion,当超过了MaxMetaspaceExpansion之后扩大需要的内存+MinMetaspaceExpansion。注意这里扩大不是指扩大metaspace的大小,而是扩大触发gc的阈值。

如果扩大gc阈值之后,还是进行了gc,那么会进行计算,可以继续commit的内存占到commit之后总内存的MinMetaspaceFreeRatio大小,如果总共commit的内存值大于当前gc的阈值,那么会对gc阈值扩大。再通俗一点解释就是,当前gc阈值是10m,commit的内存是8m,继续commit的内存(8*0.4)/(1-0.4)=5.3,超过了gc阈值10m,对gc阈值进行扩大,防止commit的内存值和gc阈值过于接近,导致gc频繁发生。

where MinMetaspaceFreeRatio is the minimum percentage of class metadata capacity
free after a GC to avoid an increase in the amount of space
(high-water-mark) allocated for class metadata that will induce a garbage
collection.

MaxMetaspaceFreeRatio:目的是为了减少空间的浪费。和上面参数正好相反。


where MaxMetaspaceFreeRatio is the maximum percentage of class metadata capacity
free after a GC to avoid a reduction in the amount of space
(high-water-mark) allocated for class metadata that will induce a garbage
collection.

我们还看到gc日志里有used,capacity,committed,reserved四个值,分别代表了什么意思?

In the line beginning with Metaspace, the used value is the amount of space used for loaded classes. The capacity value is the space available for metadata in currently allocated chunks. The committed value is the amount of space available for chunks. The reserved value is the amount of space reserved (but not necessarily committed) for metadata.

used表示metaspace已经占用的空间,capacity表示已经分配的chunk中可以给metaspace使用的空间。commited表示已经分配的空间,包括空闲的chunk。reserved表示操作系统预留的所有空间。

可以参考这个回答:https://stackoverflow.com/questions/40891433/understanding-metaspace-line-in-jvm-heap-printout

这张图表述的很明确了。

参数和每种空间分配结合,正好解释了我为什么在计算是否会gc的时候,没有用capacity的值来计算,而是采用的commited的值来进行计算的。

发布了45 篇原创文章 · 获赞 21 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/ly262173911/article/details/84931142