Java List的SubList使用问题
一、Sublist导致OOM
代码
@Slf4j public class SubListDemo { public static void subListOOM() { List<List<Integer>> data = new ArrayList<>(); for (int i = 0; i < 1000; i++) { List<Integer> rawList = IntStream.rangeClosed(1, 100000).boxed().collect(Collectors.toList());//构建一个100000个元素的list data.add(rawList.subList(0, 1)); } log.info("data.size(): " +data.size()); } }
OOM
Exception in thread "File Watcher" java.lang.OutOfMemoryError: GC overhead limit exceeded at java.io.File.listFiles(File.java:1212) at org.springframework.boot.devtools.filewatch.FolderSnapshot.collectFiles(FolderSnapshot.java:63) at org.springframework.boot.devtools.filewatch.FolderSnapshot.collectFiles(FolderSnapshot.java:67) at org.springframework.boot.devtools.filewatch.FolderSnapshot.collectFiles(FolderSnapshot.java:67) at org.springframework.boot.devtools.filewatch.FolderSnapshot.collectFiles(FolderSnapshot.java:67) at org.springframework.boot.devtools.filewatch.FolderSnapshot.<init>(FolderSnapshot.java:58) at org.springframework.boot.devtools.filewatch.FileSystemWatcher$Watcher.getCurrentSnapshots(FileSystemWatcher.java:277) at org.springframework.boot.devtools.filewatch.FileSystemWatcher$Watcher.scan(FileSystemWatcher.java:251) at org.springframework.boot.devtools.filewatch.FileSystemWatcher$Watcher.run(FileSystemWatcher.java:236) at java.lang.Thread.run(Thread.java:748) Exception in thread "restartedMain" java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49) Caused by: java.lang.OutOfMemoryError: GC overhead limit exceeded at java.lang.Integer.valueOf(Integer.java:832) at java.util.stream.IntPipeline$$Lambda$492/1604894031.apply(Unknown Source) at java.util.stream.IntPipeline$4$1.accept(IntPipeline.java:250) at java.util.stream.Streams$RangeIntSpliterator.forEachRemaining(Streams.java:110) at java.util.Spliterator$OfInt.forEachRemaining(Spliterator.java:693) at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481) at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471) at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708) at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499) at com.example.newdemo.SubListDemo.subListOOM(SubListDemo.java:17) at com.example.newdemo.NewdemoApplication.main(NewdemoApplication.java:13)
分析
出现 OOM 的原因是,循环中的 1000 个具有 10 万个元素的 List 始终得不到回收,因为它始终被 subList 方法返回的 List 强引用。
public List<E> subList(int fromIndex, int toIndex) { subListRangeCheck(fromIndex, toIndex, size); return new SubList(this, 0, fromIndex, toIndex); }
parent 字段就是原始的 List。SubList没有copy一份自己的数据,而是完整的保留了原始的list。 SubList 是原始 List 的视图,并不是独立的 List, SubList 强引用了原始的 List,所以大量保存这样的 SubList 会导致 OOM。
解决
不直接使用 subList 方法返回的 SubList,而是重新使用 new ArrayList,在构造方法传入 SubList,来构建一个独立的 ArrayList。sublist直接释放-》原始的list也被释放。
public static void subListWithoutOOM() { List<List<Integer>> data = new ArrayList<>(); for (int i = 0; i < 1000; i++) { List<Integer> rawList = IntStream.rangeClosed(1, 100000).boxed().collect(Collectors.toList());//构建一个100000个元素的list data.add(new ArrayList<>(rawList.subList(0, 1))); } log.info("data.size(): " +data.size()); }
另外一个例子
public static void removeSubList() { List<Integer> rawList = IntStream.rangeClosed(1, 10).boxed().collect(Collectors.toList()); List<Integer> subList = rawList.subList(0, 3); subList.remove(0); rawList.forEach(System.out::print); }
2345678910
可以看到,移除sublist的元素后,直接影响到了原始list。
二、修改原始列表后SubList循环报错
测试代码
public static void addItemToOriginalList() { List<Integer> rawList = IntStream.rangeClosed(1, 10).boxed().collect(Collectors.toList()); List<Integer> subList = rawList.subList(0, 3); rawList.add(11); try { subList.forEach(System.out::print); } catch (Exception ex) { ex.printStackTrace(); } }
java.util.ConcurrentModificationException at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1239) at java.util.ArrayList$SubList.listIterator(ArrayList.java:1099) at java.util.AbstractList.listIterator(AbstractList.java:299) at java.util.ArrayList$SubList.iterator(ArrayList.java:1095) at java.lang.Iterable.forEach(Iterable.java:74) at com.example.newdemo.SubListDemo.addItemToOriginalList(SubListDemo.java:46) at com.example.newdemo.NewdemoApplication.main(NewdemoApplication.java:13) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49)
分析
modCount 的字段,表示结构性修改的次数--影响list size修改测次数,add肯定影响size,导致modCount加1.但是sublist的modCount没有变,所有抛出了异常。
代码
private void checkForComodification() { if (ArrayList.this.modCount != this.modCount) throw new ConcurrentModificationException(); }
作者:iBrake
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.