Java 8构造函数参考的可怕的性能和大堆的足迹?
我只是在我们的生产环境中有一个相当不愉快的经历,导致OutOfMemoryErrors: heapspace..
我追溯到我的使用函数中ArrayList::new
的问题。
为了validation这实际上是通过一个声明的构造函数( t -> new ArrayList<>()
)执行比正常创build更差,我写了下面的小方法:
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;
运行该方法newMethod=true;
只会在索引点击30k之后导致方法失败并返回OutOfMemoryError
。 用newMethod=false;
程序不失败,但一直在冲击着,直到被杀(索引容易达到150万)。
为什么ArrayList::new
在堆上创build如此多的Object[]
元素,导致OutOfMemoryError
这么快?
(顺便说一下 – 当集合types是HashSet
时也会发生。)
在第一种情况下( ArrayList::new
),你使用的构造函数采用初始容量参数,在第二种情况下,你不是。 初始容量较大(代码中的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
之前不分配数组。
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
。
在这个特殊情况下,构造函数的引用是不合适的。 使用lambdas代替。 Supplier<? extends V>
Supplier<? extends V>
在computeIfAbsent
使用的Supplier<? extends V>
,那么ArrayList::new
将是适当的。