Java 8 构造函数参考的可怕性能和大堆占用空间?
Horrendous performance & large heap footprint of Java 8 constructor reference?
我刚刚在我们的生产环境中遇到了一个相当不愉快的经历,导致 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
时也会发生这种情况。)
在第一种情况下 (ArrayList::new
),您使用的是 constructor,它采用初始容量参数,在第二种情况下则不是。较大的初始容量(index
在您的代码中)导致分配较大的 Object[]
,导致您的 OutOfMemoryError
s。
以下是两个构造函数的当前实现:
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
才分配数组。
computeIfAbsent
签名如下:
V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
所以 mappingFunction
是 接收 一个参数的函数。在您的情况下 K = Integer
和 V = List<Integer>
,因此签名变为(省略 PECS):
Function<Integer, List<Integer>> mappingFunction
当你在需要 Function<Integer, List<Integer>>
的地方写 ArrayList::new
时,编译器会寻找合适的构造函数,即:
public ArrayList(int initialCapacity)
所以基本上你的代码等同于
map.computeIfAbsent(index, i->new ArrayList<>(i)).add(index);
并且您的键被视为 initialCapacity
值,这会导致预先分配大小不断增加的数组,当然,这会很快导致 OutOfMemoryError
.
在这种特殊情况下,构造函数引用不合适。请改用 lambda。如果在computeIfAbsent
中使用Supplier<? extends V>
,那么ArrayList::new
是合适的。
我刚刚在我们的生产环境中遇到了一个相当不愉快的经历,导致 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
时也会发生这种情况。)
在第一种情况下 (ArrayList::new
),您使用的是 constructor,它采用初始容量参数,在第二种情况下则不是。较大的初始容量(index
在您的代码中)导致分配较大的 Object[]
,导致您的 OutOfMemoryError
s。
以下是两个构造函数的当前实现:
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
才分配数组。
computeIfAbsent
签名如下:
V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
所以 mappingFunction
是 接收 一个参数的函数。在您的情况下 K = Integer
和 V = List<Integer>
,因此签名变为(省略 PECS):
Function<Integer, List<Integer>> mappingFunction
当你在需要 Function<Integer, List<Integer>>
的地方写 ArrayList::new
时,编译器会寻找合适的构造函数,即:
public ArrayList(int initialCapacity)
所以基本上你的代码等同于
map.computeIfAbsent(index, i->new ArrayList<>(i)).add(index);
并且您的键被视为 initialCapacity
值,这会导致预先分配大小不断增加的数组,当然,这会很快导致 OutOfMemoryError
.
在这种特殊情况下,构造函数引用不合适。请改用 lambda。如果在computeIfAbsent
中使用Supplier<? extends V>
,那么ArrayList::new
是合适的。