Allocation Rate和GC(Allocation Failure)

                    Allocation Rate和GC(Allocation Failure)


Allocation Rate, 翻译为分配速率, 而不是分配率; 因为不是百分比,而是单位时间内分配的量;

高分配速率(High Allocation Rate)

分配速率(Allocation rate)表示单位时间内分配的内存量。通常使用 MB/sec作为单位, 也可以使用 PB/year 等。

分配速率过高就会严重影响程序的性能。在JVM中会导致巨大的GC开销。

如何测量分配速率?

指定JVM参数: -XX:+PrintGCDetails -XX:+PrintGCTimeStamps , 通过GC日志来计算分配速率. GC日志如下所示:

0.291: [GC (Allocation Failure) 
		[PSYoungGen: 33280K->5088K(38400K)] 
		33280K->24360K(125952K), 0.0365286 secs] 
	[Times: user=0.11 sys=0.02, real=0.04 secs] 
0.446: [GC (Allocation Failure) 
		[PSYoungGen: 38368K->5120K(71680K)] 
		57640K->46240K(159232K), 0.0456796 secs] 
	[Times: user=0.15 sys=0.02, real=0.04 secs] 
0.829: [GC (Allocation Failure) 
		[PSYoungGen: 71680K->5120K(71680K)] 
		112800K->81912K(159232K), 0.0861795 secs] 
	[Times: user=0.23 sys=0.03, real=0.09 secs]

计算 上一次垃圾收集之后,与下一次GC开始之前的年轻代使用量, 两者的差值除以时间,就是分配速率。 通过上面的日志, 可以计算出以下信息:

  1. JVM启动之后 291ms, 共创建了 33,280 KB 的对象。 第一次 Minor GC(小型GC) 完成后, 年轻代中还有 5,088 KB 的对象存活。
  2. 在启动之后 446 ms, 年轻代的使用量增加到 38,368 KB, 触发第二次GC, 完成后年轻代的使用量减少到 5,120 KB
  3. 在启动之后 829 ms, 年轻代的使用量为 71,680 KB, GC后变为 5,120 KB

可以通过年轻代的使用量来计算分配速率, 如下表所示:

Event Time Young before Young after Allocated during Allocation rate
1st GC 291ms 33,280KB 5,088KB 33,280KB 114MB/sec
2nd GC 446ms 38,368KB 5,120KB 33,280KB 215MB/sec
3rd GC 829ms 71,680KB 5,120KB 66,560KB 174MB/sec
Total 829ms N/A N/A 133,120KB 161MB/sec

通过这些信息可以知道, 在测量期间, 该程序的内存分配速率为 161 MB/sec

分配速率的意义

分配速率的变化,会增加或降低GC暂停的频率, 从而影响吞吐量。 但只有年轻代的 minor GC 受分配速率的影响, 老年代GC的频率和持续时间不受 分配速率(allocation rate)的直接影响, 而是受到 提升速率(promotion rate)的影响。
在得出 “Eden区越大越好” 这个结论前, 我们注意到, 分配速率可能会,也可能不会影响程序的实际吞吐量。 吞吐量和分配速率有一定关系, 因为分配速率会影响 minor GC 暂停, 但对于总体吞吐量的影响, 还要考虑 Major GC(大型GC)暂停, 而且吞吐量的单位不是 MB/秒, 而是系统所处理的业务量。

高分配速率对JVM的影响

首先,我们应该检查程序的吞吐量是否降低。如果创建了过多的临时对象, minor GC的次数就会增加。如果并发较大, 则GC可能会严重影响吞吐量。
 

解决方案

在某些情况下,只要增加年轻代的大小, 即可降低分配速率过高所造成的影响。增加年轻代空间并不会降低分配速率, 但是会减少GC的频率。如果每次GC后只有少量对象存活, minor GC 的暂停时间就不会明显增加。
 

过早提升(Premature Promotion)

提升速率(promotion rate), 用于衡量单位时间内从年轻代提升到老年代的数据量。一般使用 MB/sec 作为单位, 和分配速率类似。

JVM会将长时间存活的对象从年轻代提升到老年代。根据分代假设, 可能存在一种情况, 老年代中不仅有存活时间长的对象,也可能有存活时间短的对象。这就是过早提升:对象存活时间还不够长的时候就被提升到了老年代。

major GC 不是为频繁回收而设计的, 但 major GC 现在也要清理这些生命短暂的对象, 就会导致GC暂停时间过长。这会严重影响系统的吞吐量。

如何测量提升速率

可以指定JVM参数 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps , 通过GC日志来测量提升速率. JVM记录的GC暂停信息如下所示:

0.291: [GC (Allocation Failure) 
		[PSYoungGen: 33280K->5088K(38400K)] 
		33280K->24360K(125952K), 0.0365286 secs] 
	[Times: user=0.11 sys=0.02, real=0.04 secs] 
0.446: [GC (Allocation Failure) 
		[PSYoungGen: 38368K->5120K(71680K)] 
		57640K->46240K(159232K), 0.0456796 secs] 
	[Times: user=0.15 sys=0.02, real=0.04 secs] 
0.829: [GC (Allocation Failure) 
		[PSYoungGen: 71680K->5120K(71680K)] 
		112800K->81912K(159232K), 0.0861795 secs] 
	[Times: user=0.23 sys=0.03, real=0.09 secs]

从上面的日志可以得知: GC之前和之后的 年轻代使用量以及堆内存使用量。这样就可以通过差值算出老年代的使用量。GC日志中的信息可以表述为:

Event Time Young decreased Total decreased Promoted Promotion rate
(事件) (耗时) (年轻代减少) (整个堆内存减少) (提升量) (提升速率)
1st GC 291ms 28,192K 8,920K 19,272K 66.2 MB/sec
2nd GC 446ms 33,248K 11,400K 21,848K 140.95 MB/sec
3rd GC 829ms 66,560K 30,888K 35,672K 93.14 MB/sec
Total 829ms     76,792K 92.63 MB/sec

根据这些信息, 就可以计算出观测周期内的提升速率。平均提升速率为 92 MB/秒, 峰值为 140.95 MB/秒

请注意, 只能根据 minor GC 计算提升速率。 Full GC 的日志不能用于计算提升速率, 因为 major GC 会清理掉老年代中的一部分对象。

提升速率的意义

和分配速率一样, 提升速率也会影响GC暂停的频率。但分配速率主要影响 minor GC, 而提升速率则影响 major GC 的频率。有大量的对象提升,自然很快将老年代填满。 老年代填充的越快, 则 major GC 事件的频率就会越高。
此前说过, full GC 通常需要更多的时间, 因为需要处理更多的对象, 还要执行碎片整理等额外的复杂过程。

过早提升的影响

一般来说,过早提升的症状表现为以下形式:

  1. 短时间内频繁地执行 full GC。
  2. 每次 full GC 后老年代的使用率都很低, 在10-20%或以下。
  3. 提升速率接近于分配速率。

     

解决方案

简单来说, 要解决这类问题, 需要让年轻代存放得下暂存的数据。是增加年轻代的大小, 设置JVM启动参数, 类似这样: -Xmx64m -XX:NewSize=32m, 程序在执行时, Full GC 的次数自然会减少很多, 只会对 minor GC的持续时间产生影响。

项目GC日志一直打印出Allocation Failure的日志,以其中一行为例来解读下日志信息:

[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(stop-the-word)。

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]

real:指的是操作从开始到结束所经过的墙钟时间(WallClock Time)
user:指的是用户态消耗的CPU时间;
sys:指的是内核态消耗的CPU时间。
墙钟时间包括各种非运算的等待耗时,例如等待磁盘I/O、等待线程阻塞,而CPU时间不包括这些耗时,但当系统有多CPU或者多核的话,多线程操作会叠加这些CPU时间,所以看到user或sys时间超过real时间是完全正常的。
user + sys 就是CPU花费的实际时间,注意这个值统计了所有CPU上的时间,如果进程工作在多线程的环境下,叠加了多线程的时间,这个值是会超出 real 所记录的值的,即 user + sys >= real 。
多次real time时间大于usr time + sys time,表明可能有两个问题,一个是IO操作密集,另一个是cpu(分配)的额度不够。

分析下可以得出结论:

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

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

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

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

解决方式:直接修改配置参数 -Xmn4g  (这里需要根据需求来设置)

有时候,增加新生代堆内存并不能解决办法。
当minor GC 的频率太高了,这说明创建了大量的对象。另外, 如果年轻代在 GC 之后的使用量又很低, 也没有 full GC 发生。 这表明, GC对吞吐量造成了严重的影响。
可以通过分配分析其找出大部分垃圾产生的位置,对代码处理对象进行优化。
刚巧今天遇到一个线上bug:
今天项目一直在报full GC,是结算时,内存存入了大量的trade数据,现在新建一个settlementTrade对象,只存必要的字段,然后然后将--xx:xmx 和--xx:xms 从10G调整为16G,新生代xmn不变,他准备上线,我等下瞄瞄他优化后的结果,循环full gc,每次都得stw, 严重影响了吞吐量.
系统内存存入的数据量变少, 内存也扩大。

你可能感兴趣的:(Java_JVM)