List.subList操作还会导致OOM?

首先我们来看一个例子

先定义一个存放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());
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值