ArrayList#subList這四個坑,一不小心就中招
Hollis的新書限時折扣中,一本深入講解Java基礎(chǔ)的干貨筆記!
一、使用不當(dāng)引起內(nèi)存泄露
先給大家看一段簡單但是比較有意思的代碼
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監(jiān)控內(nèi)存情況:
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的老年代當(dāng)i循環(huán)到大概60萬左右的時候就已經(jīng)打爆了,而我們當(dāng)前堆中的最大的對象是allFailedList最多也是60萬個Long型的List,粗略的計算一下也只有幾十MB,完全不至于打爆內(nèi)存。那我們就有理由懷疑上面的這段代碼產(chǎn)生了內(nèi)存泄露了。
回到ArrayList#subList的實現(xiàn)代碼:
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;
}
}
可以看到,每次調(diào)用ArrayList#subList的時候都會生成一個SubList對象,而這個對象的parent屬性值卻持有原ArrayList的引用,這樣一來就說得通了,allFailedList持有歷次調(diào)用queryOrder產(chǎn)生的List對象,這些對象最終都轉(zhuǎn)移到了老年代而得不到釋放。
二、使用不當(dāng)引起死循環(huán)
再看一段代碼:
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;
}
}
如果我說上面的這段代碼是一個死循環(huán),你會感到奇怪么。回到subList的實現(xiàn)
// AbstractList
public boolean add(E e) {
add(size(), e);
return true;
}
然后會調(diào)用到ArrayList的方法
public void add(int index, E e) {
rangeCheckForAdd(index);
checkForComodification();
parent.add(parentOffset + index, e);
this.modCount = parent.modCount;
this.size++;
}
可以看到,調(diào)用subList的add其實是在原ArrayList中增加元素,因此原arrayList.size()會一直變大,最終導(dǎo)致死循環(huán)。
三、無法對subList和原List做結(jié)構(gòu)性修改
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);
}
要注意,調(diào)用原數(shù)組的add方法時已經(jīng)修改了原數(shù)組的modCount屬性,當(dāng)程序執(zhí)行到打印subList這行代碼時會調(diào)用Sublist#toString方法,到最后會調(diào)用到下面這個私有方法:
private void checkForComodification() {
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
}
根據(jù)前面分析,原ArrayList的modCount屬性已經(jīng)自增,所以ArrayList.this.modCount != this.modCount執(zhí)行的結(jié)果是true,最終拋出了ConcurrentModificationException異常。
關(guān)于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被結(jié)構(gòu)性修改的次數(shù),所謂結(jié)構(gòu)性修改是指能夠改變List大小的操作
如果提前沒有知識儲備,這類異常是比較難排查的
四、作為RPC接口入?yún)r序列化失敗
從上面SubList的定義可以看出來,SubList并沒有實現(xiàn)Serializable接口,因此在一些依賴Java原生序列化協(xié)議的RPC的框架中會序列化失敗,如Dubbo等。
五、最佳實踐
subList設(shè)計之初是作為原List的一個視圖,經(jīng)常在只讀的場景下使用,這和大多數(shù)人理解的不太一樣,即便只在只讀的場景下使用,也容易產(chǎn)生內(nèi)存泄露,況且這個視圖的存在還不允許原List和SubList做結(jié)構(gòu)性修改,個人認為subList這個Api的設(shè)計糟糕透了,盡量在代碼中避免直接使用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核心技術(shù)》已經(jīng)上市了,上市后一直蟬聯(lián)京東暢銷榜中,目前正在6折優(yōu)惠中,想要入手的朋友千萬不要錯過哦~長按二維碼即可購買~
長按掃碼享受6折優(yōu)惠
往期推薦

3000幀動畫圖解MySQL為什么需要binlog、redo log和undo log

知乎熱議:月薪 2~3W 的碼農(nóng),怎樣度過一天?

從實現(xiàn)原理講,Nacos 為什么這么強
有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號
好文章,我在看??
