1.现场监控截图
由上面两张图可以看出,
1. old区内存使用并不多,为何会fullgc。
2.年轻代确实没有进行gc,所以可以确定当时确实进行了fullgc。
2.gc log
我们服务器使用的是g1回收器,找到当时时间节点的gc log,截图如下:
3.分析log,提出问题
我们从监控中可以看到,fullgc时:
总计堆内存2051m -> 142m, 总共回收了 1927m
在年轻代,eden回收了 1532m, survivor44m,总计 1576m
说明老年代总共只回收了 1927 - 1576 = 351m
这个和监控中是能对应上的。 关键问题是,为什么old区使用很小,会进行fullgc?而且这个fullgc回收old区很少,Eden区回收很多?
4.什么时候会gc?
这个问题可以直接参考 https://blog.csdn.net/jiguansheng/article/details/105406343
我们知道会有内存担保,当Eden区尝试分配对象空间不足时会放在old区,old区也无法容纳才会fullgc
但是,上面说的情况对于g1情况是不适用的。我们来看看g1的full gc逻辑
以下解释来自网络
转移失败(Evacuation Failure)是指当G1无法在堆空间中申请新的分区时,G1便会触发担保机制,执行一次STW式的、单线程的Full GC。Full GC会对整堆做标记清除和压缩,最后将只包含纯粹的存活对象
G1在以下场景中会触发Full GC,同时会在日志中记录to-space-exhausted以及Evacuation Failure:
从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
从老年代分区转移存活对象时,无法找到可用的空闲分区
分配巨型对象时在老年代无法找到足够的连续分区
G1启动标记周期,但在Mix GC之前,老年代就被填满,这时候G1会放弃标记周期。这种情形下,需要增加堆大小,或者调整周期(例如增加线程数-XX:ConcGCThreads等)。
由于G1的应用场合往往堆内存都比较大,所以Full GC的收集代价非常昂贵,应该避免Full GC的发生。
4.3.4 异常情况
并发标记周期开始后的FULL GC
启动了标记周期,但是在并发标记完成之前,就发生了Full GC,日志常常如下所示:
GC concurrent-mark-start开始之后就发生了FULL GC,这说明老年代空间不够用了,可能原因:老年代分区的回收速度比较慢,对象过快得从新生代晋升到老年代,有很多大对象直接在老年代分配。针对上述原因,可能需要做的调整有:调大整个堆的大小、更快得触发并发回收周期、让更多的回收线程参与到垃圾收集的动作中。
混合收集模式中的FULL GC
在一次混合收集之后跟着一条FULL GC,这意味着混合收集的速度太慢,在老年代释放出足够多的分区之前,应用程序就来请求比当前剩余可分配空间大的内存。针对这种情况我们可以做的调整:增加每次混合收集收集掉的老年代分区个数;增加并发标记的线程数;提高混合收集发生的频率。
疏散失败(转移失败)
在新生代垃圾收集快结束时,找不到可用的分区接收存活下来的对象,发送full gc。
增加整个堆的大小——通过增加 -XX:G1ReservePercent选项的值(并相应增加总的堆大小)。
通过减少 -XX:InitiatingHeapOccupancyPercent 提前启动标记周期。
巨型对象分配失败
如果在GC日志中看到莫名其妙的FULL GC日志,又对应不到上述讲过的几种情况,那么就可以怀疑是巨型对象分配导致的,这里我们可以考虑使用 jmap命令进行堆dump,然后通过MAT对堆转储文件进行分析。
以上参考https://km.sankuai.com/page/208897718
而且g1的fullgc也不是自己实现的,是调用的其他回收器,多为 serial
5.结论
根据上面的fullgc可能的情况:
G1在以下场景中会触发Full GC,同时会在日志中记录to-space-exhausted以及Evacuation Failure:
从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
从老年代分区转移存活对象时,无法找到可用的空闲分区
分配巨型对象时在老年代无法找到足够的连续分区
G1启动标记周期,但在Mix GC之前,老年代就被填满,这时候G1会放弃标记周期。这种情形下,需要增加堆大小,或者调整周期(例如增加线程数-XX:ConcGCThreads等)。
由于G1的应用场合往往堆内存都比较大,所以Full GC的收集代价非常昂贵,应该避免Full GC的发生。
结合10:27分的请求情况,可以认为返回结果是 一个31行61列的map
在本地模拟其占有内存:约为10m。模拟代码
@Test
public void testCsv() throws InterruptedException {
// 31 61
Thread.sleep(20000);
String field = "fieldfieldfieldfieldfieldfield";
String value = "valuevaluevaluevaluevaluevalue";
List rows = Lists.newArrayList();
for (int i = 0; i < 32; i++){
Map row = Maps.newHashMap();
for (int j = 0; j < 62; j ++){
row.put(value + j, field);
}
rows.add(row);
}
System.out.println(rows);
Thread.sleep(20000);
List rows2 = Lists.newArrayList();
for (int i = 0; i < 32; i++){
Map row = Maps.newHashMap();
for (int j = 0; j < 62; j ++){
row.put(value + j, field);
}
rows2.add(row);
}
Thread.sleep(20000);
}
使用内存情况用jconsole查看的情况
可以知道这样的请求结果约占10m。
我们在gclog中看到 g1的每个region是4m,此时我们的对象符合巨型对象(> region 50%)
理论上应该是此时巨型对象过多无法分配导致的
6.反思
排查链条过长
对于fullgc发生的情况一刀切,忽略了各回收器之间的差异
由于本人水平有限,难免有误,欢迎指正交流。