先给大家看一段简单但是比较有意思的代码
- public class OrderService {
-
- public static void main(String[] args) {
- OrderService orderService = new OrderService();
- orderService.process();
- }
-
- public void process() {
- List<Long> orderIdList = queryOrder();
- List<List<Long>> allFailedList = new ArrayList<>();
- for(int i = 0; i < Integer.MAX_VALUE; i++) {
- System.out.println(i);
- List<Long> failedList = doProcess(orderIdList);
- allFailedList.add(failedList);
- }
- }
-
- private List<Long> doProcess(List<Long> orderIdList) {
- List<Long> failedList = new ArrayList<>();
- for (Long orderId : orderIdList) {
- if (orderId % 2 == 0) {
- failedList.add(orderId) ;
- }
- }
- // 只取一个失败的订单id做分析
- return failedList.subList(0, 1);
- }
-
- private List<Long> queryOrder() {
- List<Long> 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<E> subList(int fromIndex, int toIndex) {
- subListRangeCheck(fromIndex, toIndex, size);
- return new SubList(this, 0, fromIndex, toIndex);
- }
-
- private class SubList extends AbstractList<E> implements RandomAccess {
- private final AbstractList<E> parent;
- private final int parentOffset;
- private final int offset;
- int size;
-
- SubList(AbstractList<E> 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<Long> arrayList = init();
- List<Long> 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<Long> init() {
- List<Long> 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<String> listArr = new ArrayList<>();
- listArr.add("Delhi");
- listArr.add("Bangalore");
- listArr.add("New York");
- listArr.add("London");
-
- List<String> 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
有两条最佳实践:
ArrayList
中- ArrayList myArrayList = new ArrayList();
- ArrayList part1 = new ArrayList(myArrayList.subList(0, 25));
- ArrayList part2 = new ArrayList(myArrayList.subList(26, 51));
lambda
表达式- dataList.stream().skip(5).limit(10).collect(Collectors.toList());
- dataList.stream().skip(30).limit(10).collect(Collectors.toList());
完
感谢阅读,更多的java课程学习路线,笔记,面试等架构资料,需要的同学可以私信我(资料)即可免费获取!