JVM会将长时间存活的对象从年轻代提升到老年代。根据分代假设, 可能存在一种情况, 老年代中不仅有存活时间长的对象,也可能有存活时间短的对象。这就是过早提升:对象存活时间还不够长的时候就被提升到了老年代。
major GC 不是为频繁回收而设计的, 但 major GC 现在也要清理这些生命短暂的对象, 就会导致GC暂停时间过长。这会严重影响系统的吞吐量。
可以指定JVM参数 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps
, 通过GC日志来测量提升速率. JVM记录的GC暂停信息如下所示:
从上面的日志可以得知: GC之前和之后的 年轻代使用量以及堆内存使用量。这样就可以通过差值算出老年代的使用量。GC日志中的信息可以表述为:
根据这些信息, 就可以计算出观测周期内的提升速率。平均提升速率为 , 峰值为 140.95 MB/秒
。
和分配速率一样, 提升速率也会影响GC暂停的频率。但分配速率主要影响 , 而提升速率则影响 major GC 的频率。有大量的对象提升,自然很快将老年代填满。 老年代填充的越快, 则 major GC 事件的频率就会越高。
此前说过, full GC 通常需要更多的时间, 因为需要处理更多的对象, 还要执行碎片整理等额外的复杂过程。
让我们看一个。 这个程序创建/获取大量的对象/数据,并暂存到集合之中, 达到一定数量后进行批处理:
此 Demo 程序 受到过早提升的影响。下文将进行验证并给出解决办法。
- 每次 full GC 后老年代的使用率都很低, 在10-20%或以下。
- 提升速率接近于分配速率。
要演示这种情况稍微有点麻烦, 所以我们使用特殊手段, 让对象提升到老年代的年龄比默认情况小很多。指定GC参数 , 运行程序之后,可以看到下面的GC日志:
乍一看似乎不是过早提升的问题。事实上,在每次GC之后老年代的使用率似乎在减少。但反过来想, 要是没有对象提升或者提升率很小, 也就不会看到这么多的 Full GC 了。
简单解释一下这里的GC行为: 有很多对象提升到老年代, 同时老年代中也有很多对象被回收了, 这就造成了老年代使用量减少的假象. 但事实是大量的对象不断地被提升到老年代, 并触发 full GC。
简单来说, 要解决这类问题, 需要让年轻代存放得下暂存的数据。有两种简单的方法:
一是增加年轻代的大小, 设置JVM启动参数, 类似这样: -Xmx64m -XX:NewSize=32m
, 程序在执行时, Full GC 的次数自然会减少很多, 只会对 minor GC的持续时间产生影响:
如果都不可行, 就只能优化数据结构, 减少内存消耗。但总体目标依然是一致的: 让临时数据能够在年轻代存放得下。