首先我们来看一个例子
先定义一个存放Integer的List的List,循环1000次,每次循环都会产生一个,size为100万的list,使用subList()获取只包含一个数字的List并存入data中。这时data里的数据应该长生么样呢?1000个只包含一个数字的List吗?我们来运行一下吧。
public class SubListDemo {
private static List<List<Integer>> data = new ArrayList<>();
public static void oom() {
for (int i = 0; i < 1000; i++) {
List<Integer> rawList = IntStream.rangeClosed(1, 1000000).boxed().collect(Collectors.toList());
data.add(rawList.subList(0, 1));
}
System.out.println(data);
}
public static void main(String[] args) {
oom();
}
}
运行结果
呀,他报错了,这是为啥呢,为啥不是1000个只包含一个数字的List呢?
分析
这1000次循环中的产生的一个个size为1000万的list始终被subList()返回的List强引用,使他得不到回收造成的。接下来我们来看一看为什么返回的子list会强引用原来的list。
我们点进入ArrayList.subList()的源码来看一哈(部分源码截取)
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
transient Object[] elementData;
private int size;
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList<>(this, fromIndex, toIndex);
}
private static class SubList<E> extends AbstractList<E> implements RandomAccess {
private final ArrayList<E> root;
private final SubList<E> parent;
private final int offset;
private int size;
public SubList(ArrayList<E> root, int fromIndex, int toIndex) {
this.root = root;
this.parent = null;
this.offset = fromIndex;
this.size = toIndex - fromIndex;
this.modCount = root.modCount;
}
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
public Iterator<E> iterator() {
return listIterator();
}
public ListIterator<E> listIterator(int index) {
checkForComodification();
rangeCheckForAdd(index);
...
}
private void checkForComodification() {
if (root.modCount != modCount)
throw new ConcurrentModificationException();
}
}
}
我们直接看subList()方法,在这里我们会发现:
第一:subList()返回的并不是一个ArrayList,他返回的是一个
SubList类,并且在初始化时传入了this。
第二:SubList是ArrayList的一个内部类。再看一下他的构造方法会发现他的root就是原来的List,初始化时并没有将截取的元素复制到新的变量中。由此可见SubList就是原来List的视图,并不是新的List,双方对集合中元素的修改是会互相影响的。并且因为SubList对原来的List有强引用,导致这些原始集合不能被垃圾回收,所以导致了OOM。
第三:还是在SubList的构造方法中我们会发现this.modCount = root.modCount;SubList的modCount就是原来集合的modCount。modCount是在ArrayList中维护的一个字段,表示集合的结构性修改的次数。所以对于原始集合的add,remove操作时一定会改变原始集合modCount的值,而经过subList()后得到的List的modCount是不会改变的。
验证
接下来我们通过另一个例子来验证一下,SubList是否就是原来List的视图,双方对集合中元素的修改会不会互相影响,SubList和原始集合的modCount的变化是怎么样的。
public static void subListTest() {
List<Integer> list = IntStream.rangeClosed(1, 10).boxed().collect(Collectors.toList());
List<Integer> subList = list.subList(1, 4);
List<Integer> arrayList1 = list.stream().skip(1).limit(3).collect(Collectors.toList());
System.out.println(subList);
subList.remove(1);
System.out.println("删除[1]位置的元素后的subList:" + subList);
System.out.println("删除[1]位置的元素后的list:" + list);
subList.add(14);
System.out.println("对subList添加一个元素后的list:" + list);
System.out.println("对subList添加一个元素后的subList:" + subList);
list.add(0);
System.out.println("对list添加一个元素后的list:" + list);
System.out.println("对list添加一个元素后的subList:" + subList);
}
运行结果
结果分析
第一点:根据结果可知,对subList删除一个元素后,原始集合list的"3"元素也不见了。对subList添加一个元素后,原始集合list也多了一个"14"元素。由此可见,双方对集合中元素的修改是会互相影响的。
第二点:我们会发现,在对list添加一个元素后输出subList是报了ConcurrentModificationException,根据报错提示我们找到SubList.checkForComodification(),进入方法后会发现root.modCount != modCount时会抛出ConcurrentModificationException。debug一下看到root.modCount = 13而modCount = 12,所以在对list进行add操作时只修改了list的modCount值,并未修改subList的modCount值。
总结
List.subList操作导致OOM的根本原因就是分片后的List对饮食集合的强引用。为了避免这种情况的发生,在获取到分片后的List后,我们不要直接使用这个集合进行操作,可以使用一个新的变量保存分片后的list。
// 方法一
List<Integer> arrayList = new ArrayList<>(rawList.subList(0, 2));
// 方法二
List<Integer> arrayList1 = list.stream().skip(1).limit(3).collect(Collectors.toList());