我在生产环境中遇到了相当不愉快的经历,导致OutOfMemoryErrors: heapspace..
我将问题追溯到我在函数中使用ArrayList::new
。
要通过声明的构造函数(t -> new ArrayList<>()
)验证这实际上比正常创建更糟糕,我编写了以下小方法:
public class TestMain {
public static void main(String[] args) {
boolean newMethod = false;
Map<Integer,List<Integer>> map = new HashMap<>();
int index = 0;
while(true){
if (newMethod) {
map.computeIfAbsent(index, ArrayList::new).add(index);
} else {
map.computeIfAbsent(index, i->new ArrayList<>()).add(index);
}
if (index++ % 100 == 0) {
System.out.println("Reached index "+index);
}
}
}
}
使用newMethod=true;
运行方法会导致方法在索引达到30k后立即失败OutOfMemoryError
。使用newMethod=false;
程序不会失败,但会一直冲击直到被杀(索引容易达到150万)。
为什么ArrayList::new
会在堆上创建如此多的Object[]
元素,导致OutOfMemoryError
如此之快?
(顺便说一下 - 当集合类型为HashSet
时也会发生。)
答案 0 :(得分:93)
在第一种情况下(ArrayList::new
),你使用constructor获取初始容量参数,在第二种情况下你不是。较大的初始容量(代码中为index
)会导致分配较大的Object[]
,从而产生OutOfMemoryError
个。
以下是两个构造函数的当前实现:
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
在HashSet
中发生了类似的事情,除了在调用add
之前未分配数组。
答案 1 :(得分:76)
computeIfAbsent
签名如下:
V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
因此mappingFunction
是接收一个参数的函数。在您的情况K = Integer
和V = List<Integer>
中,签名变为(省略PECS):
Function<Integer, List<Integer>> mappingFunction
当你在需要ArrayList::new
的地方写Function<Integer, List<Integer>>
时,编译器会查找合适的构造函数:
public ArrayList(int initialCapacity)
基本上你的代码等同于
map.computeIfAbsent(index, i->new ArrayList<>(i)).add(index);
并且您的密钥被视为initialCapacity
值,这会导致预先分配不断增加的大小的数组,当然,这会非常快地导致OutOfMemoryError
。
在这种特殊情况下,构造函数引用不合适。请改用lambdas。如果在Supplier<? extends V>
中使用了computeIfAbsent
,那么ArrayList::new
就是合适的。