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


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

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

    1. public class OrderService {
    2.     
    3.     public static void main(String[] args) {
    4.         OrderService orderService = new OrderService();
    5.         orderService.process();
    6.     }
    7.     
    8.     public void process() {
    9.         List<Long> orderIdList = queryOrder();
    10.         List<List<Long>> allFailedList = new ArrayList<>();
    11.         for(int i = 0; i < Integer.MAX_VALUE; i++) {
    12.             System.out.println(i);
    13.             List<Long> failedList = doProcess(orderIdList);
    14.             allFailedList.add(failedList);
    15.         }
    16.     }
    17.     
    18.     private List<Long> doProcess(List<Long> orderIdList) {
    19.         List<Long> failedList = new ArrayList<>();
    20.         for (Long orderId : orderIdList) {
    21.             if (orderId % 2 == 0) {
    22.                 failedList.add(orderId) ;
    23.             }
    24.         }
    25.         // 只取一个失败的订单id做分析
    26.         return failedList.subList(01);
    27.     }
    28.     
    29.     private List<Long> queryOrder() {
    30.         List<Long> orderIdList = new ArrayList<>();
    31.         for (int i = 0; i < 1000; i++) {
    32.             orderIdList.add(RandomUtils.nextLong());
    33.         }
    34.         return orderIdList;
    35.     }
    36. }

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

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

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

    回到ArrayList#subList的实现代码:

    1. public List<E> subList(int fromIndex, int toIndex) {
    2.     subListRangeCheck(fromIndex, toIndex, size);
    3.     return new SubList(this0, fromIndex, toIndex);
    4. }
    5. private class SubList extends AbstractList<E> implements RandomAccess {
    6.     private final AbstractList<E> parent;
    7.     private final int parentOffset;
    8.     private final int offset;
    9.     int size;
    10.     SubList(AbstractList<E> parent,
    11.             int offset, int fromIndex, int toIndex) {
    12.       this.parent = parent;
    13.       this.parentOffset = fromIndex;
    14.       this.offset = offset + fromIndex;
    15.       this.size = toIndex - fromIndex;
    16.       this.modCount = ArrayList.this.modCount;
    17.     }
    18. }

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

    二、使用不当引起死循环

    再看一段代码:

    1. public class SubListDemo {
    2.     public static void main(String[] args) {
    3.         List<Long> arrayList = init();
    4.         List<Long> subList = arrayList.subList(01);
    5.         for (int i = 0; i < arrayList.size(); i++) {
    6.             if (arrayList.get(i) % 2 == 0) {
    7.                 subList.add(arrayList.get(i));
    8.             }
    9.         }
    10.     }
    11.     private static List<Long> init() {
    12.         List<Long> arrayList = new ArrayList<>();
    13.         arrayList.add(RandomUtils.nextLong());
    14.         arrayList.add(RandomUtils.nextLong());
    15.         arrayList.add(RandomUtils.nextLong());
    16.         arrayList.add(RandomUtils.nextLong());
    17.         arrayList.add(RandomUtils.nextLong());
    18.         return arrayList;
    19.     }
    20. }

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

    1. // AbstractList
    2. public boolean add(E e) {
    3.     add(size(), e);
    4.     return true;
    5. }

    然后会调用到ArrayList的方法

    1. public void add(int index, E e) {
    2.     rangeCheckForAdd(index);
    3.     checkForComodification();
    4.     parent.add(parentOffset + index, e);
    5.     this.modCount = parent.modCount;
    6.     this.size++;
    7. }

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

    三、无法对subList和原List做结构性修改

    1. public static void main(String[] args) {
    2.     List<String> listArr = new ArrayList<>();
    3.     listArr.add("Delhi");
    4.     listArr.add("Bangalore");
    5.     listArr.add("New York");
    6.     listArr.add("London");
    7.     List<String> listArrSub = listArr.subList(13);
    8.     System.out.println("List-: " + listArr);
    9.     System.out.println("Sub List-: " + listArrSub);
    10.     //Performing Structural Change in list.
    11.     listArr.add("Mumbai");
    12.     System.out.println("\nAfter Structural Change...\n");
    13.     System.out.println("List-: " + listArr);
    14.     System.out.println("Sub List-: " + listArrSub);
    15. }

    这段代码最后会抛出ConcurrentModificationException

    1. List-: [Delhi, Bangalore, New York, London]
    2. Sub List-: [Bangalore, New York]
    3. After Structural Change...
    4. List-: [Delhi, Bangalore, New York, London, Mumbai]
    5. Exception in thread "main" java.util.ConcurrentModificationException
    6.     at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1231)
    7.     at java.util.ArrayList$SubList.listIterator(ArrayList.java:1091)
    8.     at java.util.AbstractList.listIterator(AbstractList.java:299)
    9.     at java.util.ArrayList$SubList.iterator(ArrayList.java:1087)
    10.     at java.util.AbstractCollection.toString(AbstractCollection.java:454)
    11.     at java.lang.String.valueOf(String.java:2982)
    12.     at java.lang.StringBuilder.append(StringBuilder.java:131)
    13.     at infosys.Research.main(Research.java:26)

    简单看下ArrayList的源码:

    1. public boolean add(E e) {
    2.     ensureCapacityInternal(size + 1);  // Increments modCount!!
    3.     elementData[size++] = e;
    4.     return true;
    5. }
    6. private void ensureCapacityInternal(int minCapacity) {
    7.     ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    8. }
    9. private void ensureExplicitCapacity(int minCapacity) {
    10.     // 注意这行对原list的modCount这个变量做了自增操作
    11.     modCount++;
    12.     // overflow-conscious code
    13.     if (minCapacity - elementData.length > 0)
    14.         grow(minCapacity);
    15. }

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

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

    根据前面分析,原ArrayListmodCount属性已经自增,所以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的一个视图,经常在只读的场景下使用,这和大多数人理解的不太一样,即便只在只读的场景下使用,也容易产生内存泄露,况且这个视图的存在还不允许原ListSubList做结构性修改,个人认为subList这个Api的设计糟糕透了,尽量在代码中避免直接使用ArrayList#subList,获取ListsubList有两条最佳实践:

    5.1 拷贝到新的ArrayList

    1. ArrayList myArrayList = new ArrayList();
    2. ArrayList part1 = new ArrayList(myArrayList.subList(025));
    3. ArrayList part2 = new ArrayList(myArrayList.subList(2651));

    5.2 使用lambda表达式

    1. dataList.stream().skip(5).limit(10).collect(Collectors.toList());
    2. dataList.stream().skip(30).limit(10).collect(Collectors.toList());

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

  • 相关阅读:
    用HTML+CSS做一个简单好看的汽车网页
    【深度学习】用Pytorch完成MNIST手写数字数据集的训练和测试
    Wise Care 365 Pro v6.3.9.617 系统优化软件
    HTML5七夕情人节表白网页制作【唯美3D相册】HTML+CSS+JavaScript
    又一超好用的 Python 数据处理工具 Mito 前来报到
    golang使用channel通道实现非阻塞队列和超时阻塞队列
    泛微OA建模查询中自定义按钮弹出自定义对话框+调用新建表单卡片中的保存功能
    敏捷整洁之道
    普通jar和SpringBootjar的区别
    B-树(B-Tree)与二叉搜索树(BST):讲讲数据库和文件系统背后的原理(读写比较大块数据的存储系统数据结构与算法原理)...
  • 原文地址:https://blog.csdn.net/hahazz233/article/details/125425426