欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JVM Full GC分析

程序员文章站 2022-03-02 22:09:20
...

JVM的GC关于对象Age问题以及JVM的内存分配策略

Eden中比较频繁的GC,每次GC都会导致内存分布的变化。对象的计数器:对象的Age是标记的关键。年青代的GC又叫minor GC。对象GC年龄阈值默认是15。每次GC后,还存在,对象Age加1。

实际运行的时候,Survive区域中,对象不一定要达到阀值就进入Old Generation。有这样一种情况,就是Survive中有这样一组对象,其Age是相同的,其总大小等于或超过Survive空间的一半,就会集体晋升为Old Generation对象。是一种程序运行的动态调整。

新生代GC时,对象进入Old Generation不是一帆风顺,如果要进入对象的大小超过Old Generation剩余空间大小,就会发生Full GC。Full GC的耗时是新生代GC的好几十倍。

如果进入Old Generation的对象少于Old Generation的剩余大小,是否需要Full GC呢?有参数HandlePromotionFailure。

HandlePromotionFailure:是否担保失败,如果是进行担保失败,就不需要进行Full GC。如果不允许担保失败,就需要进行Full GC。我们一般打开这个开关,有OOM的风险,冒风险来减少Full GC。

大对象直接进入Old Generation,主要是一些数组,大的字符串,因为打对象会占用连续的内存空间,在Eden内进行大对象的拷贝效率比较低,作为性能考量,直接进入Old Generation更加合理。

Desired survivor size 80510976 bytes, new threshold 15 (max 15)
- age   1:   22760688 bytes,   22760688 total
- age   2:   11241200 bytes,   34001888 total
- age 1表示 经过1次MinorGC的对象存在22760688 B,
- age 2表示 年龄为2的对象 存在有11241200B,
此处经过两次GC仍存活的对象表示有11241200 bytes,其中34001888 total表示,目前经过多次GC后仍存活对象的总和,
为34001888 B,此处则当然是经过一次GC的对象的大小22760688 bytes,加上经过两次GC的对象的大小11241200 bytes的总和,34001888 total,所以此处total 表示当前年轻代存活对象的
大小,
- 对象晋升到老年代的年龄除了是超出所设置大小15的晋升外,
- 当前年龄大小的总和超出Survivor大小的一半是也会自动晋升

GC日志

[GC (Allocation Failure) [ParNew: 367523K->1293K(410432K), 0.0023988 secs] 522739K->156516K(1322496K), 0.0025301 secs] [Times: user=0.04 sys=0.00, real=0.01 secs]

GC:

表明进行了一次垃圾回收,前面没有Full修饰,表明这是一次Minor GC ,注意它不表示只GC新生代,并且现有的不管是新生代还是老年代都会STW。

Allocation Failure:

表明本次引起GC的原因是因为在年轻代中没有足够的空间能够存储新的数据了。

ParNew:

表明本次GC发生在年轻代并且使用的是ParNew垃圾收集器。ParNew是一个Serial收集器的多线程版本,会使用多个CPU和线程完成垃圾收集工作(默认使用的线程数和CPU数相同,可以使用-XX:ParallelGCThreads参数限制)。该收集器采用复制算法回收内存,期间会停止其他工作线程,即Stop The World。

367523K->1293K(410432K):单位是KB

三个参数分别为:GC前该内存区域(这里是年轻代)使用容量,GC后该内存区域使用容量,该内存区域总容量。

0.0023988 secs:

该内存区域GC耗时,单位是秒

522739K->156516K(1322496K):

三个参数分别为:堆区垃圾回收前的大小,堆区垃圾回收后的大小,堆区总大小。

0.0025301 secs:

该内存区域GC耗时,单位是秒

[Times: user=0.04 sys=0.00, real=0.01 secs]:

分别表示用户态耗时,内核态耗时和总耗时

分析下可以得出结论:

该次GC新生代减少了367523-1293=366239K

Heap区总共减少了522739-156516=366223K

366239 – 366223 =16K,说明该次共有16K内存从年轻代移到了老年代,可以看出来数量并不多,说明都是生命周期短的对象,只是这种对象有很多。

我们需要的是尽量避免Full GC的发生,让对象尽可能的在年轻代就回收掉,所以这里可以稍微增加一点年轻代的大小,让那17K的数据也保存在年轻代中。

以CMS为例,补充一些知识点介绍:
复制算法介绍:

因为新生代对象生命周期一般很短,现在一般将该内存区域划分为三块部分,一块大的叫Eden,两块小的叫Survivor。他们之间的比例一般为8:1:1。

使用的时候只使用Eden + 一块Survivor。用Eden区用满时会进行一次minor gc,将存活下面的对象复制到另外一块Survivor上。如果另一块Survivor放不下(对应虚拟机参数为 XX:TargetSurvivorRatio,默认50,即50%),对象直接进入老年代。

为什么要有两个Survivor:

主要是为了解决内存碎片化和效率问题。如果只有一个Survivor时,每触发一次minor gc都会有数据从Eden放到Survivor,一直这样循环下去。

一. 并发模式失败(concurrent mode failure)

2016-02-21T13:53:11.775+0800: 171471.055: [GC2016-02-21T13:53:11.775+0800: 171471.055: [ParNew (promotion failed)
Desired survivor size 53673984 bytes, new threshold 6 (max 6)
- age   1:    4030872 bytes,    4030872 total
- age   2:    5959704 bytes,    9990576 total
- age   3:    4628680 bytes,   14619256 total
- age   4:    8773080 bytes,   23392336 total
- age   5:    7707144 bytes,   31099480 total
- age   6:   10890224 bytes,   41989704 total
: 914528K->907344K(943744K), 1.0312010 secs]2016-02-21T13:53:12.807+0800: 171472.086: [CMS2016-02-21T13:53:14.455+0800: 171473.735: [CMS-concurrent-sweep: 1.684/2.743 secs] [Times: user=3.69 sys=0.36, real=2.74 secs]
 (concurrent mode failure): 1451903K->475795K(1560576K), 3.9644230 secs] 2356134K->475795K(2504320K), [CMS Perm : 78413K->78413K(159744K)], 4.9959570 secs] [Times: user=5.65 sys=0.34, real=5.00 secs]
Heap after GC invocations=88669 (full 46):
 par new generation   total 943744K, used 0K [0x0000000757000000, 0x0000000797000000, 0x0000000797000000)
  eden space 838912K,   0% used [0x0000000757000000, 0x0000000757000000, 0x000000078a340000)
  from space 104832K,   0% used [0x000000078a340000, 0x000000078a340000, 0x00000007909a0000)
  to   space 104832K,   0% used [0x00000007909a0000, 0x00000007909a0000, 0x0000000797000000)
 concurrent mark-sweep generation total 1560576K, used 475795K [0x0000000797000000, 0x00000007f6400000, 0x00000007f6400000)
 concurrent-mark-sweep perm gen total 159744K, used 78413K [0x00000007f6400000, 0x0000000800000000, 0x0000000800000000)

两个原因:
在 CMS 启动过程中,新生代提升速度过快,老年代收集速度赶不上新生代提升速度
在 CMS 启动过程中,老年代碎片化严重,无法容纳新生代提升上来的大对象

发送这种情况,应用线程将会全部停止(相当于网站这段时间无法响应用户请求),进行压缩式垃圾收集(回退到 Serial Old 算法)

解决办法:
新生代提升过快问题:(1)如果频率太快的话,说明空间不足,首先可以尝试调大新生代空间和晋升阈值。(2)如果内存有限,可以设置 CMS 垃圾收集在老年代占比达到多少时启动来减少问题发生频率(越早启动问题发生频率越低,但是会降低吞吐量,具体得多调整几次找到平衡点),参数如下:如果没有第二个参数,会随着 JVM 动态调节 CMS 启动时间
-XX:CMSInitiatingOccupancyFraction=68 (默认是 68)
-XX:+UseCMSInitiatingOccupancyOnly

老年代碎片严重问题:(1)如果频率太快或者 Full GC 后空间释放不多的话,说明空间不足,首先可以尝试调大老年代空间(2)如果内存不足,可以设置进行 n 次 CMS 后进行一次压缩式 Full GC,参数如下:
-XX:+UseCMSCompactAtFullCollection:允许在 Full GC 时,启用压缩式 GC
-XX:CMSFullGCBeforeCompaction=n 在进行 n 次,CMS 后,进行一次压缩的 Full GC,用以减少 CMS 产生的碎片

二. 提升失败(promotion failed)
在 Minor GC 过程中, Survivor Unused 可能不足以容纳 Eden 和另一个 Survivor 中的存活对象, 那么多余的将被移到老年代, 称为 过早提升(Premature Promotion)。 这会导致老年代中短期存活对象的增长, 可能会引发严重的性能问题。 再进一步, 如果老年代满了, Minor GC 后会进行 Full GC, 这将导致遍历整个堆, 称为 提升失败(Promotion Failure)。

2016-01-07T18:54:26.948+0800: 18782.967: [GC2016-04-07T18:54:26.948+0800: 18782.967: [ParNew (promotion failed)
Desired survivor size 117833728 bytes, new threshold 10 (max 10)
- age   1:    6141680 bytes,    6141680 total
- age   2:    6337936 bytes,   12479616 total
- age   3:     549120 bytes,   13028736 total
- age   4:      87768 bytes,   13116504 total
- age   5:     221384 bytes,   13337888 total
- age   6:     934168 bytes,   14272056 total
- age   7:     146072 bytes,   14418128 total
- age   8:     626064 bytes,   15044192 total
- age   9:     398000 bytes,   15442192 total
- age  10:     429616 bytes,   15871808 total
: 1969227K->1929200K(2071808K), 0.7452140 secs]2016-01-07T18:54:27.693+0800: 18783.713: [CMS: 1394703K->632845K(2097152K), 4.0993640 secs] 3301676K->632845K(4168960K), [CMS Perm : 77485K->77473K(159744K)], 4.8450240 secs] [Times: user=5.18 sys=0.56, real=4.84 secs]
Heap after GC invocations=5847 (full 7):
 par new generation   total 2071808K, used 0K [0x00000006e9c00000, 0x0000000776400000, 0x0000000776400000)
  eden space 1841664K,   0% used [0x00000006e9c00000, 0x00000006e9c00000, 0x000000075a280000)
  from space 230144K,   0% used [0x0000000768340000, 0x0000000768340000, 0x0000000776400000)
  to   space 230144K,   0% used [0x000000075a280000, 0x000000075a280000, 0x0000000768340000)
 concurrent mark-sweep generation total 2097152K, used 632845K [0x0000000776400000, 0x00000007f6400000, 0x00000007f6400000)
 concurrent-mark-sweep perm gen total 159744K, used 77473K [0x00000007f6400000, 0x0000000800000000, 0x0000000800000000)

提升失败原因:Minor GC 时发现 Survivor 空间放不下,而老年代的空闲也不够
新生代提升太快
老年代碎片太多,放不下大对象提升(表现为老年代还有很多空间但是,出现了 promotion failed)

解决方法:
两条和上面 concurrent mode failure 一样
另一条,是因为 Survivor Unused 不足,那么可以尝试调大 Survivor 来尝试下

三. 在 GC 的时候其他系统活动影响
有些时候系统活动诸如内存换入换出(vmstat)、网络活动(netstat)、I/O (iostat)在 GC 过程中发生会使 GC 时间变长。

前提是你的服务器上是有 SWAP 区域(用 top、 vmstat 等命令可以看出)用于内存的换入换出,那么操作系统可能会将 JVM 中不活跃的内存页换到 SWAP 区域用以释放内存给线程使用(这也透露出内存开始不够用了)。内存换入换出是一个开销巨大的磁盘操作,比内存访问慢好几个数量级。

{Heap before GC invocations=132 (full 0):
par new generation total 2696384K, used 2696384K [0xfffffffc20010000, 0xfffffffce0010000, 0xfffffffce0010000)
eden space 2247040K, 100% used [0xfffffffc20010000, 0xfffffffca9270000, 0xfffffffca9270000)
from space 449344K, 100% used [0xfffffffca9270000, 0xfffffffcc4940000, 0xfffffffcc4940000)
to space 449344K, 0% used [0xfffffffcc4940000, 0xfffffffcc4940000, 0xfffffffce0010000)
concurrent mark-sweep generation total 9437184K, used 1860619K [0xfffffffce0010000, 0xffffffff20010000, 0xffffffff20010000)
concurrent-mark-sweep perm gen total 1310720K, used 511451K [0xffffffff20010000, 0xffffffff70010000, 0xffffffff70010000)
2013-07-17T03:58:06.601-0700: 51522.120: [GC Before GC: : 2696384K->449344K(2696384K), 29.4779282 secs] 4557003K->2326821K(12133568K) ,29.4795222 secs] [Times: user=915.56 sys=6.35, real=29.48 secs]

再看看此时的 vmstat 命令中 si、so 列的数值,如果数值大说明换入换出严重,这是内存不足的表现。

解决方法:减少线程,这样可以降低内存换入换出;增加内存;如果是 JVM 内存设置过大导致线程所用内存不足,则适当调低 -Xmx 和 -Xms。

五. 总结
长时间停顿问题的排查及解决首先需要一定的信息和方法论:
详细的 GC 日志
借助 Linux 平台下的 iostat、vmstat、netstat、mpstat 等命令监控系统情况
使用 GCHisto 这个 GC 图形用户界面工具,可以统计出 Minor GC 及 Full GC 频率及时长分布,可参考:http://blog.csdn.net/wenniuwuren/article/details/50760259
查看 GC 日志中是否出现了上述的典型内存异常问题(promotion failed, concurrent mode failure),整体来说把上述两个典型内存异常情况控制在可接受的发生频率即可,对 CMS 碎片问题来说杜绝以上问题似乎不太可能,只能靠 G1 来解决了
是不是 JVM 本身的 bug 导致的
如果程序没问题,参数调了几次还是不能解决,可能说明流量太大,需要加机器把压力分散到更多 JVM 上

零. 新生代调优规律
增大新生代空间, Minor GC 频率减少, Minor GC 时间上升。 减少新生代空间, Minor GC 频率上升, Minor GC 时间下降

一. 新生代典型问题
先看一段 GC 日志:新生代使用 ParNew, 老年代使用 CMS

{Heap before GC invocations=0 (full 0):
 par new generation   total 943744K, used 838912K [0x0000000757000000, 0x0000000797000000, 0x0000000797000000)
  eden space 838912K, 100% used [0x0000000757000000, 0x000000078a340000, 0x000000078a340000)
  from space 104832K,   0% used [0x000000078a340000, 0x000000078a340000, 0x00000007909a0000)
  to   space 104832K,   0% used [0x00000007909a0000, 0x00000007909a0000, 0x0000000797000000)
 concurrent mark-sweep generation total 1560576K, used 0K [0x0000000797000000, 0x00000007f6400000, 0x00000007f6400000)
 concurrent-mark-sweep perm gen total 159744K, used 38069K [0x00000007f6400000, 0x0000000800000000, 0x0000000800000000)
2016-01-19T14:15:34.532+0800: 13.812: [GC2016-02-19T14:15:34.532+0800: 13.812: [ParNew
Desired survivor size 53673984 bytes, new threshold 1 (max 6)
- age   1:   55521392 bytes,   55521392 total
: 838912K->54474K(943744K), 0.0914620 secs] 838912K->54474K(2504320K), 0.0916240 secs] [Times: user=0.67 sys=0.06, real=0.09 secs]
Heap after GC invocations=1 (full 0):
 par new generation   total 943744K, used 54474K [0x0000000757000000, 0x0000000797000000, 0x0000000797000000)
  eden space 838912K,   0% used [0x0000000757000000, 0x0000000757000000, 0x000000078a340000)
  from space 104832K,  51% used [0x00000007909a0000, 0x0000000793ed2ae0, 0x0000000797000000)
  to   space 104832K,   0% used [0x000000078a340000, 0x000000078a340000, 0x00000007909a0000)
 concurrent mark-sweep generation total 1560576K, used 0K [0x0000000797000000, 0x00000007f6400000, 0x00000007f6400000)
 concurrent-mark-sweep perm gen total 159744K, used 38069K [0x00000007f6400000, 0x0000000800000000, 0x0000000800000000)
}
{Heap before GC invocations=1 (full 0):
 par new generation   total 943744K, used 893386K [0x0000000757000000, 0x0000000797000000, 0x0000000797000000)
  eden space 838912K, 100% used [0x0000000757000000, 0x000000078a340000, 0x000000078a340000)
  from space 104832K,  51% used [0x00000007909a0000, 0x0000000793ed2ae0, 0x0000000797000000)
  to   space 104832K,   0% used [0x000000078a340000, 0x000000078a340000, 0x00000007909a0000)
 concurrent mark-sweep generation total 1560576K, used 0K [0x0000000797000000, 0x00000007f6400000, 0x00000007f6400000)
 concurrent-mark-sweep perm gen total 159744K, used 53249K [0x00000007f6400000, 0x0000000800000000, 0x0000000800000000)
2016-01-19T14:15:41.943+0800: 21.222: [GC2016-02-19T14:15:41.943+0800: 21.223: [ParNew
Desired survivor size 53673984 bytes, new threshold 1 (max 6)
- age   1:  107256200 bytes,  107256200 total
: 893386K->104832K(943744K), 1.2389070 secs] 893386K->210614K(2504320K), 1.2391870 secs] [Times: user=2.89 sys=0.35, real=1.24 secs]
Heap after GC invocations=2 (full 0):
 par new generation   total 943744K, used 104832K [0x0000000757000000, 0x0000000797000000, 0x0000000797000000)
  eden space 838912K,   0% used [0x0000000757000000, 0x0000000757000000, 0x000000078a340000)
  from space 104832K, 100% used [0x000000078a340000, 0x00000007909a0000, 0x00000007909a0000)
  to   space 104832K,   0% used [0x00000007909a0000, 0x00000007909a0000, 0x0000000797000000)
  concurrent mark-sweep generation total 1560576K, used 105782K [0x0000000797000000, 0x00000007f6400000, 0x00000007f6400000)
 concurrent-mark-sweep perm gen total 159744K, used 53249K [0x00000007f6400000, 0x0000000800000000, 0x0000000800000000)
}

可以明显看出上述 GC 日志包含两次 Minor GC。 注意到第二次 Minor GC 的情况, 日志打出 “Desired survivor size 53673984 bytes”, 但是却存活了 “- age 1: 107256200 bytes, 107256200 total” 这么多。 可以看出明显的新生代的 Survivor 空间不足。正因为 Survivor 空间不足, 那么从 Eden 存活下来的和原来在 Survivor 空间中不够老的对象占满 Survivor 后, 就会提升到老年代, 可以看到这一轮 Minor GC 后老年代由原来的 0K 占用变成了 105782K 占用, 这属于一个典型的 JVM 内存问题, 称为 “premature promotion”(过早提升)。

"premature promotion” 在短期看来不会有问题, 但是经常性的 "premature promotion”, 最总会导致大量短期对象被提升到老年代, 最终导致老年代空间不足, 引发另一个 JVM 内存问题 “promotion failure”(提升失败: 即老年代空间不足以容乃 Minor GC 中提升上来的对象)。 “promotion failure” 发生就会让 JVM 进行一次 CMS 垃圾收集进而腾出空间接受新生代提升上来的对象, CMS 垃圾收集时间比 Minor GC 长, 导致吞吐量下降、 时延上升, 将对用户体验造成影响。

二. 新生代调优建议
对于上述的新生代问题, 如果服务器内存足够用, 建议是直接增大新生代空间(如 -Xmn)。如果内存不够用, 则增加 Survivor 空间, 减少 Eden 空间, 但是注意减少 Eden 空间会增加 Minor GC 频率, 要考虑到应用对延迟和吞吐量的指标最终是否符合。

要增大多少 Survivor 空间? 需要观察多次 Minor GC 过程, 看 Minor GC 后存活下来的对象大小, 最终确定 Survivor 的合适大小。 整个调优过程可能需要几次调整, 才能找到比较合适的值。调整几次后, 如果内存还是不够用, 就要需要考虑增大服务器内存, 或者把负载分担到更多的 JVM 实例上。

Survivor 空间计算公式: survivor 空间大小 = -Xmn[value] / (-XX:SurvivorRatio= + 2)

Survivor空间溢出实例
https://segmentfault.com/a/1190000004657756

相关标签: 每天一道面试题