踩坑分享|终于明白为啥面试老是有人问 subList 了,原来这玩意会 OOM!
最近刚做到一个内存分页的需求,自测了几次就 OOM 了,找了半天原因,终于把这个坑填上来,下面整理一下发出来,各位小伙伴引以为鉴。
我们经常会使用 List.subList 方法对 List 进行切片,比如取前十个元素出来用,但是和 Arrays.asList 的问题类似(具体文章可以看 慎用 ArrayList,全是坑!),List.subList 返回的子 List 不是一个全新地址的 ArrayList,这个子 List 会和原始 List 相互影响。
如果不注意,很可能会因此产生 OOM 问题。
话不多说,先复现问题。如下代码所示,定义一个名为 data 的静态 List 用来存放 List<Integer>
类型,循环 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 的源码,看看为什么会是这样:
-
ArrayList 维护了一个 int 类型的 modCount
的字段,表示 List 结构性修改的次数。所谓结构性修改,指的是影响 List 大小的修改,所以 add 操作必然会改变 modCount 的值。 -
分析 subList 方法可以看到,获得的 List 其实是创建了一个内部类 SubList,并不是普通的 ArrayList。 -
在初始化内部类 SubList 的时候传入了 this,这个 SubList 中的 parent 字段就是原始的 List,初始化的时候,并没有把原始 List 中的元素复制到独立的变量中保存,所以双方对元素的修改都会互相影响。而且 SubList 强引用了原始的 List,所以大量保存这样的 SubList 其实也保存了大量原始的 List,从而导致 OOM。 -
分析 listIterator 方法可知,遍历 SubList 的时候会先获得迭代器,比较原始 ArrayList modCount 的值和 SubList 当前 modCount 的值,如果不想等,就会抛出 ConcurrentModificationException
异常。所以上述实验代码,我们在获得了 SubList 为原始 List 新增了一个元素,修改了原始 List 的 modCount,所以判等失败抛出异常。
综上,既然 SubList 和原始 List 会相互影响,那么避免相互影响的修复方式有两种:
-
不直接使用 subList 方法返回的 SubList,而是重新使用 new ArrayList,在构造方法传入 SubList,来构建一个独立的 ArrayList:
List<Integer> subList = new ArrayList<>(list.subList(1, 4));
-
对于 Java 8 使用 Stream 的 skip 和 limit API 来跳过流中的元素,以及限制流中元素的个数,同样可以达到 SubList 切片的目的:
List<Integer> subList = list.stream().skip(1).limit(3).collect(Collectors.toList());
简历修改:
内推信息: