最近刚做到一个内存分页的需求,自测了几次就 OOM 了,找了半天原因,终于把这个坑填上来,下面整理一下发出来,各位小伙伴引以为鉴。
我们经常会使用 List.subList 方法对 List 进行切片,比如取前十个元素出来用,但是和 Arrays.asList 的问题类似(具体文章可以看 慎用 ArrayList,全是坑!),List.subList 返回的子 List 不是一个全新地址的 ArrayList,这个子 List 会和原始 List 相互影响。
如果不注意,很可能会因此产生 OOM 问题。
话不多说,先复现问题。如下代码所示,定义一个名为 data 的静态 List 用来存放 List 类型,循环 1000 次,每次都从一个具有 100 万个 Integer 的 List 中(即代码中的 rawList),使用 subList 方法获得一个只包含一个数字的子 List,并把这个子 List 加入 data 变量:
图片
看起来,这个 data 变量里面最终保存的只是 1000 个具有 1 个元素的 List 而已,并不会出现什么问题啊。
但是,代码在运行到一段时间后,可以看到在我的机器上是第 159 次循环后发生了 OOM:
图片
出现 OOM 的原因是,循环中的 1000 个具有 100 万个元素的 List 始终得不到回收,因为它始终被 subList 方法返回的 List 强引用。
subList 返回的子 List 为啥会强引用原始的 List?再来做个实验看下:
首先初始化一个包含数字 1 到 10 的 ArrayList,然后通过调用 subList 方法取出 2、3、4,随后删除这个 SubList 中的元素数字 3。可以看到原始 List 中数字 3 被删除了,说明删除子 List 中的元素影响到了原始 List:
图片
图片
继续看,我们为原始的 ArrayList 增加一个元素数字 0,然后遍历 SubList 输出所有元素。代码运行后报错 java.util.ConcurrentModificationException:
图片
图片
分析下 ArrayList 的源码,看看为什么会是这样:
图片
综上,既然 SubList 和原始 List 会相互影响,那么避免相互影响的修复方式有两种:
List subList = new ArrayList(list.subList(1, 4));
List subList = list.stream().skip(1).limit(3).collect(Collectors.toList());