栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

ArrayList#subList这四个坑,一不小心就中招

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

ArrayList#subList这四个坑,一不小心就中招

一、使用不当引起内存泄露

先给大家看一段简单但是比较有意思的代码

public class OrderService {
    
    public static void main(String[] args) {
        OrderService orderService = new OrderService();
        orderService.process();
    }
    
    public void process() {
        List orderIdList = queryOrder();
        List> allFailedList = new ArrayList<>();
        for(int i = 0; i < Integer.MAX_VALUE; i++) {
            System.out.println(i);
            List failedList = doProcess(orderIdList);
            allFailedList.add(failedList);
        }
    }
    
    private List doProcess(List orderIdList) {
        List failedList = new ArrayList<>();
        for (Long orderId : orderIdList) {
            if (orderId % 2 == 0) {
                failedList.add(orderId) ;
            }
        }
        // 只取一个失败的订单id做分析
        return failedList.subList(0, 1);
    }
    
    private List queryOrder() {
        List orderIdList = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            orderIdList.add(RandomUtils.nextLong());
        }
        return orderIdList;
    }
}

如果你在本地的机器上运行这段代码,并且打开arthas监控内存情况:

Memory                            used        total      max         usage      
heap                              2742M       3643M      3643M       75.28%     
ps_eden_space                     11M         462M       468M        2.52%      
ps_survivor_space                 0K          460288K    460288K     0.00%      
ps_old_gen                        2730M       2731M      2731M       99.99%     
nonheap                           28M         28M        -1          97.22%     
code_cache                        5M          5M         240M        2.19%      
metaspace                         20M         20M        -1          97.19%     
compressed_class_space            2M          2M         1024M       0.25%      
direct                            0K          0K         -           0.00%      
mapped                            0K          0K         -           0.00% 

不到3GB的老年代当i循环到大概60万左右的时候就已经打爆了,而我们当前堆中的最大的对象是allFailedList最多也是60万个Long型的List,粗略的计算一下也只有几十MB,完全不至于打爆内存。那我们就有理由怀疑上面的这段代码产生了内存泄露了。

回到ArrayList#subList的实现代码:

public List subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);
    return new SubList(this, 0, fromIndex, toIndex);
}

private class SubList extends AbstractList implements RandomAccess {
    private final AbstractList parent;
    private final int parentOffset;
    private final int offset;
    int size;

    SubList(AbstractList parent,
            int offset, int fromIndex, int toIndex) {
      this.parent = parent;
      this.parentOffset = fromIndex;
      this.offset = offset + fromIndex;
      this.size = toIndex - fromIndex;
      this.modCount = ArrayList.this.modCount;
    }
}

可以看到,每次调用ArrayList#subList的时候都会生成一个SubList对象,而这个对象的parent属性值却持有原ArrayList的引用,这样一来就说得通了,allFailedList持有历次调用queryOrder产生的List对象,这些对象最终都转移到了老年代而得不到释放。

二、使用不当引起死循环

再看一段代码:

public class SubListDemo {

    public static void main(String[] args) {
        List arrayList = init();
        List subList = arrayList.subList(0, 1);
        for (int i = 0; i < arrayList.size(); i++) {
            if (arrayList.get(i) % 2 == 0) {
                subList.add(arrayList.get(i));
            }
        }
    }

    private static List init() {
        List arrayList = new ArrayList<>();
        arrayList.add(RandomUtils.nextLong());
        arrayList.add(RandomUtils.nextLong());
        arrayList.add(RandomUtils.nextLong());
        arrayList.add(RandomUtils.nextLong());
        arrayList.add(RandomUtils.nextLong());
        return arrayList;
    }
}

如果我说上面的这段代码是一个死循环,你会感到奇怪么。回到subList的实现

// AbstractList
public boolean add(E e) {
    add(size(), e);
    return true;
}

然后会调用到ArrayList的方法

public void add(int index, E e) {
    rangeCheckForAdd(index);
    checkForComodification();
    parent.add(parentOffset + index, e);
    this.modCount = parent.modCount;
    this.size++;
}

可以看到,调用subList的add其实是在原ArrayList中增加元素,因此原arrayList.size()会一直变大,最终导致死循环。

三、无法对subList和原List做结构性修改
public static void main(String[] args) {
    List listArr = new ArrayList<>();
    listArr.add("Delhi");
    listArr.add("Bangalore");
    listArr.add("New York");
    listArr.add("London");

    List listArrSub = listArr.subList(1, 3);

    System.out.println("List-: " + listArr);
    System.out.println("Sub List-: " + listArrSub);

    //Performing Structural Change in list.
    listArr.add("Mumbai");

    System.out.println("nAfter Structural Change...n");

    System.out.println("List-: " + listArr);
    System.out.println("Sub List-: " + listArrSub);
}

这段代码最后会抛出ConcurrentModificationException

List-: [Delhi, Bangalore, New York, London]
Sub List-: [Bangalore, New York]

After Structural Change...

List-: [Delhi, Bangalore, New York, London, Mumbai]
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1231)
    at java.util.ArrayList$SubList.listIterator(ArrayList.java:1091)
    at java.util.AbstractList.listIterator(AbstractList.java:299)
    at java.util.ArrayList$SubList.iterator(ArrayList.java:1087)
    at java.util.AbstractCollection.toString(AbstractCollection.java:454)
    at java.lang.String.valueOf(String.java:2982)
    at java.lang.StringBuilder.append(StringBuilder.java:131)
    at infosys.Research.main(Research.java:26)

简单看下ArrayList的源码:

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

private void ensureCapacityInternal(int minCapacity) {
    ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private void ensureExplicitCapacity(int minCapacity) {
    // 注意这行对原list的modCount这个变量做了自增操作
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

要注意,调用原数组的add方法时已经修改了原数组的modCount属性,当程序执行到打印subList这行代码时会调用Sublist#toString方法,到最后会调用到下面这个私有方法:

private void checkForComodification() {
    if (ArrayList.this.modCount != this.modCount)
        throw new ConcurrentModificationException();
}

根据前面分析,原ArrayList的modCount属性已经自增,所以ArrayList.this.modCount != this.modCount执行的结果是true,最终抛出了ConcurrentModificationException异常。

关于modCount这个属性,Oracle的文档中也有详细的描述

The number of times this list has been structurally modified. Structural modifications are those that change the size of the list.

翻译过来就是:

modCount记录的是List被结构性修改的次数,所谓结构性修改是指能够改变List大小的操作

如果提前没有知识储备,这类异常是比较难排查的

四、作为RPC接口入参时序列化失败

从上面SubList的定义可以看出来,SubList并没有实现Serializable接口,因此在一些依赖Java原生序列化协议的RPC的框架中会序列化失败,如Dubbo等。

五、最佳实践

subList设计之初是作为原List的一个视图,经常在只读的场景下使用,这和大多数人理解的不太一样,即便只在只读的场景下使用,也容易产生内存泄露,况且这个视图的存在还不允许原List和SubList做结构性修改,个人认为subList这个Api的设计糟糕透了,尽量在代码中避免直接使用ArrayList#subList,获取List的subList有两条最佳实践:

5.1 拷贝到新的ArrayList中
ArrayList myArrayList = new ArrayList();
ArrayList part1 = new ArrayList(myArrayList.subList(0, 25));
ArrayList part2 = new ArrayList(myArrayList.subList(26, 51));
5.2 使用lambda表达式
dataList.stream().skip(5).limit(10).collect(Collectors.toList());
dataList.stream().skip(30).limit(10).collect(Collectors.toList());

完

感谢阅读,更多的java课程学习路线,笔记,面试等架构资料,需要的同学可以私信我(资料)即可免费获取!

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/988609.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号