Memory Leak Caused by subList
A common mistake with subList() is inadvertently retaining a reference to the original large list, leading to an OutOfMemoryError. Consider this example:
@Slf4j
public class SubListDemo {
public static void subListOOM() {
List<List<Integer>> data = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
// Create a list with 100,000 elements
List<Integer> rawList = IntStream.rangeClosed(1, 100000)
.boxed()
.collect(Collectors.toList());
data.add(rawList.subList(0, 1));
}
log.info("data.size(): " + data.size());
}
}
Running this method triggers:
Exception in thread "restartedMain" java.lang.OutOfMemoryError: GC overhead limit exceeded
at java.lang.Integer.valueOf(Integer.java:832)
...
The root cause lies in the implementation of subList():
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
The returned SubList holds a strong reference to the original list via its parent field. It does not create a copy of the elements; it is merely a view. Therefore, keeping many SubList instances prevents the original large lists from being garbage collected, causing OOM.
Solution: Wrap the sublist in a new ArrayList to obtain an independent copy:
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());
data.add(new ArrayList<>(rawList.subList(0, 1)));
}
log.info("data.size(): " + data.size());
}
Now once the loop iteration finishes, rawList becomes eligible for garbage collection becuase the SubList is not retained.
Side Effects on the Original List
Because subList() returns a view, modifications to the sublist directly affect the original list. For example:
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);
}
// Output: 2345678910
Removing the first element from subList also removes it from rawList.
ConcurrentModificationException When Modifying the Original List
Structural modifications to the original list after creating a SubList invalidate the sublist, leading to a ConcurrentModificationException.
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();
}
}
This throws:
java.util.ConcurrentModificationException
at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1239)
...
Internaly, ArrayList tracks structural modifications (e.g., add, remove) with modCount. When the original list's modCount changes, the sublist’s modCount becomes stale, and the sublist’s methods (like iterator()) call checkForComodification():
private void checkForComodification() {
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
}
Best Practices
- Independence: If you need a separate list, always copy the
subList()result:new ArrayList<>(original.subList(...)). - No Side Effects: Avoid modifying the original list while a sublist is in use, or ensure the sublist is discarded after such modifications.
- Resource Management: For long-lived sublists that reference large parent lists, copy them to prevent memory leaks.