阳光明媚的一天,突然收到告警,监控发现实例突发 long GC:
随机查看一个 POD,存在突发高量分配,而随后顺利回收:
查看日志输出java.lang.OutOfMemoryError: Java heap space,临近活动这可不是个好消息。检查实例出入流量,发现并没有明显波动,那么即有可能是某处迭代代码导致。然而,悲催的是,好不容易下载下来的 heap dump 提示文件不完整,人在风中凌乱!!!本以为疑无路,柳暗花明的是在检查 GC log,发现如下堆栈 ( 本意是想检查是否是 Soft Reference 的影线,结果歪打正着 ):
2022-06-13T18:48:30.346+0800: 240693.001: Total time for which application threads were stopped: 0.7906625 seconds, Stopping threads took: 0.0003087 seconds
==WARNING== allocating large array--thread_id[0x00007f564a0ce7f0]--thread_name[http-nio-8080-exec-195]--array_size[905969680 bytes]--array_length[226492416 elememts]
os_prio=0 tid=0x00007f564a0ce7f0 nid=0x3e2 runnable
at java.util.Arrays.copyOf(Arrays.java:3181)
at java.util.ArrayList.grow(ArrayList.java:265)
at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:239)
at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:231)
at java.util.ArrayList.addAll(ArrayList.java:583)
从第二行的信息中可以明显看到原因:allocating large array,结合堆栈加日志定位问题点:
public GoodsFilter addDeliveryAttrs(List<Integer> deliveryAttrs) {
if (CollectionUtils.isEmpty(this.deliveryAttrs)) {
this.deliveryAttrs = deliveryAttrs;
} else if (CollectionUtils.isNotEmpty(deliveryAttrs)) {
this.deliveryAttrs.addAll(deliveryAttrs);
}
return this;
}
咋一看没有看出似乎并没有什么问题,而实际上问题点就在第三行的赋值:入参 deliveryAttrs 并不是local scope 变量而是 request scope 变量,这会导致在外层迭代调用该方法的时候 deliveryAttrs 每次的元素翻倍!!!问题 "根因"知道了,解决也很简单 ( 从简考虑 ) -- 每次新建对象赋值:
public GoodsFilter addDeliveryAttrs(List<Integer> deliveryAttrs) {
if (CollectionUtils.isEmpty(this.deliveryAttrs)) {
this.deliveryAttrs = Lists.newArrayList(deliveryAttrs);
} else if (CollectionUtils.isNotEmpty(deliveryAttrs)) {
this.deliveryAttrs = Lists.newArrayList(this.deliveryAttrs);
this.deliveryAttrs.addAll(deliveryAttrs);
}
return this;
}
问题到此就结束了么?并没有!想想这么一个问题:能不能从根本上解决问题,避免出现这种无意引用带来的问题?一个思路就如 String -- 使用不可变设计,每次操作产生新对象,这样就可以有效避免上述问题 ( 当然,这也意味着更大的分配量以及回收量,但在业务快速迭代效率为主要矛盾点下,是一个实在方案 ),这也正是函数式编程的思想之一,同时不变性也意味着并发安全!