从 OOM 到不可变

101 阅读2分钟

阳光明媚的一天,突然收到告警,监控发现实例突发 long GC: image.png

随机查看一个 POD,存在突发高量分配,而随后顺利回收:

image.png

查看日志输出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 -- 使用不可变设计,每次操作产生新对象,这样就可以有效避免上述问题 ( 当然,这也意味着更大的分配量以及回收量,但在业务快速迭代效率为主要矛盾点下,是一个实在方案 ),这也正是函数式编程的思想之一,同时不变性也意味着并发安全!