在Java和Android编程中,我们经常使用类似ArrayList,HashMap等这些容器。这些容器少则存储几条,多则上千甚至更多。作为性能调优的一部分,容器调优往往被我们忽略,本文将尝试探索阐述一些关于容器调优中的扩容问题。虽然以Java为例,但是也同样适用于其他编程语言。
首先以我们最常用的ArrayList为例,它是一个基于数组的List实现。
1 2 3 4 5 6 | public static void main(String[] args) { ArrayList<Object> collection = new ArrayList(); for (int i = 0; i< 1000; i++) { collection.add(new Object()); } } |
---|
以上代码很简单,不用赘述。那我们使用NetBeans的profile插件 来看一下关于Object对象分配的stacktrace
从stacktrace中,我们可以发现
以上我们可以推断,ArrayList对象发生了扩容操作。因为使用无参的构造方法,会初始化一个存储容量为0的数组。
如下代码为ArrayList的构造方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 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; } |
---|
然而想要容量1000个Object实例,这个过程中则需要不断的扩容,在这个过程中发生了以下几点
除此之外,扩容还会增加CPU高速缓存的未命中率。因为
在JVM中,一般来说,由于对象和其字段常常都需要同时引用,将对象和其字段尽可能放在内存中相邻位置能够减少CPU高速缓存的未命中率。
而ArrayList扩容后的新数组可能不在于该对象相邻,所以扩容理论上会增加CPU高速缓存的未命中率。
注意:上面提到的都是CPU高速缓存的未命中率,不是命中率。
HashMap作为一个高效的key-value的容器,内部也维护了一个Entry数组,也存在扩容的问题。
然而,HashMap为了更加有效的避免数组冲突,引入了两个概念。
举个例子
因此说HashMap更容易触发扩容,但是这其实是一种在hash与容量占用的一种平衡。
SQLiteDatabase提供了方便的ContentValues简化了我们处理列名与值的映射,ContentValues内部采用了HashMap来存储Key-Value数据,ContentValues的初始容量是8,如果当添加的数据超过8之前,则会进行双倍扩容操作,
因此建议对ContentValues填入的内容进行估量,根据实际需要的字段数量,设置合理的初始化数量。
数组的一大优点就是随机访问很高效,这是链表所无法匹敌的。
但是并不是所有的时候都数组都有明显优势
一些替代方案
关于扩容的问题就是以上内容,当我们无论是使用任何数据结构时都需要考虑到具体的环境和需要,确保能够做到最优。