文章目录
前言
ArrayList是十分常用的集合类,很好用而且也不难用。这篇文章主要是对ArrayList的源码进行阅读,了解它常用方法的设计,做到不仅要会用,还要懂得为何要这样用。虽然ArrayList每一个Java程序员都会使用,但了解一下它的源码实现也是非常有必要的!其实主要还是因为面试要用到
ArrayList是什么
ArrayList是一个数组,底层使用Object[] elementData
进行存储数据。普通的数组长度是固定的,而ArrayList数组的长度可以动态改变,因此使用起来更加方便。之所以可以改变,主要是在需要的时候对elementData进行修改。
与它类似的是LinkedList,二者的区别就像数组与链表的区别。ArrayList在查找和随机访问的情况下速度较快,而LinkedList在插入和删除的情况下速度较快。
特点:查询效率高,插入删除效率低,线程不安全。
Question:为什么线程不安全还要使用ArrayList?
Ans:因为我们正常使用的场景中,都是用来查询,不会涉及太频繁的增删。如果涉及频繁的增删,可以使用LinkedList。如果你需要线程安全就使用Vector,这就是三者的区别了,实际开发过程中还是ArrayList使用最多的。不存在一个集合工具是查询效率又高,增删效率也高的,还线程安全的,至于为啥大家看代码就知道了,因为数据结构的特性就是优劣共存的,想找个平衡点很难,牺牲了性能,那就安全,牺牲了安全那就快速。
ArrayList的方法
构造方法
一共有3个,其中包含1个空的构造方法,1个传入初始的容量,1个传入Collection对象作为初始化数据。
无参构造方法:给elementData赋值为空数据。
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
传入初始容量的构造方法:如果int参数为0,相当于无参构造方法,如果为负数抛出异常。值得注意的是,ArrayList()和ArrayList(0)并不等价,elementData分别会赋值为DEFAULTCAPACITY_EMPTY_ELEMENTDATA
和EMPTY_ELEMENTDATA
,后面扩容的时候会看到二者的区别。
/**
* Constructs an empty list with the specified initial capacity.
*
* @param initialCapacity the initial capacity of the list
* @throws IllegalArgumentException if the specified initial capacity
* is negative
*/
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);
}
}
传入Collection作为初始化数据:
/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
注意注释c.toArray might (incorrectly) not return Object[],c.toArray方法可能不会返回Object[]类型,这可能会导致类型错误。至于为什么Collection的toArray可能不会返回Object[]类型,这里先看ArrayList里的toArray方法:
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
里面调用了Arrays.copyOf方法,最后返回的是elementData的类型。在前面我们已经知道,elementData的类型是Object[]类型,因此最后返回的就是Object[]类型。那么什么情况下不会返回Object[]类型?我们还记得Arrays类里有一个asList方法,它会返回一个Arrays的内部类对象,也是叫ArrayList,(Arrays.ArrayList),而这个同名的ArrayList也继承了AbstractList,因此也属于Collection,但它的toArray方法如下:
public static <T> List<T> asList(T... a) { // Arrays的asList方法
return new ArrayList<>(a);
}
// Arrays的内部类
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
private final E[] a;
// ...
@Override
@SuppressWarnings("unchecked")
public <T> T[] toArray(T[] a) {
int size = size();
if (a.length < size)
return Arrays.copyOf(this.a, size,
(Class<? extends T[]>) a.getClass());
System.arraycopy(this.a, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
// ...
}
关键是看toArray方法,此处依然是调用Arrays.copfOf方法,但它传入的参数a,a的类型是E[],而不是Object[]。这就导致了Arrays.asList理应可以作为ArrayList构造方法的参数,但却会出现类型错误的情况,也就是如下例子:
public static void test1()
{
List<String> list = Arrays.asList("abc");
System.out.println(list.getClass()); // class java.util.Arrays$ArrayList
Object[] objArray = list.toArray();
System.out.println(objArray.getClass()); // class [Ljava.lang.String;
objArray[0] = new Object(); // cause ArrayStoreException
}
public static void test2()
{
List<String> dataList = new ArrayList<>(Arrays.asList("abc")); // good
System.out.println(dataList.getClass()); // class java.util.ArrayList
Object[] listToArray = dataList.toArray();
// class [Ljava.lang.Object;返回的是Object数组
System.out.println(listToArray.getClass()); // class [Ljava.lang.Object;
listToArray[0] = "";
listToArray[0] = 123;
listToArray[0] = new Object(); // all are ok
}
test2是我们的常用用法,把Arrays.asList
作为构造参数传给ArrayList,此时可以传入Object类型对象。但如果直接像test1那样做,会因为Arrays.toArray方法返回的类型是实际类型而不是Object类型,导致出错。在JDK修改此错误之前,test2会报test1的错误,因为类型不一致。但这个bug已经是很久之前的了,早就修复了,此处只是理解这一句注释的由来。
Question:为什么底层实现是elementData,但是数组的长度却可以动态改变?
Ans:ArrayList可以通过构造方法在初始化的时候指定底层数组的大小,而当元素的数量超出数组大小时,底层会修改elementData,实现扩容。数组默认的DEFAULT_CAPACITY是10,每一次扩容会增长为原来的1.5倍。
Question:调用构造方法ArrayList(int initialCapacity)会不会初始化数组大小?
Ans:不会。在构造方法里可以看到,它只是声明里缓冲数组elementData的长度,但实际上此时数组仍然是空的。所以如果我们进行get,add(int, E)等方法,都会报错。调用构造方法之后,可以看到size依然为0。
List<Integer> list = new ArrayList<>(100);
System.out.println(list.size()); // 0
list.add(1, 1); // throws exception
add方法
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
对于第一个add方法,首先调用ensureCapacityInternal,确保数组可以容纳size + 1的数据。如果无法容纳,则对数组进行扩容。接着就是对elementData数组进行简单的赋值操作。而且也看得出来,add方法如果不指定index参数,那么就是在列表的末尾进行添加元素。
而第二个add方法,多了一个rangeCheckForAdd
方法,确保index没有越界。add的过程是先把原数组index开始到结尾的数据移动到从index + 1开始的位置,这时候index位置就空出来,再执行elementData[index] = element
就完成了元素的插入,最后再修改size。移动数组的算法使用的是System.arraycopy方法。如图演示:
①要在index 5的位置上插入元素
②把从index 5开始的元素移动到index 5 + 1之后的位置(实际上这时候index 5依然是5,下图应该是1,2,3,4,5,5,6,7,8,9)
③在index 5的位置上修改为要插入的元素
显然,因为需要移动数组,复制数组,因此add操作的效率比较低。但是如果要往越靠近数组末尾的位置添加元素,这时候复制移动数组的大小就越小。所以如果是在数组的末尾进行add,那么效率还是比较高的。
而第三个addAll方法,就是通过for-each,也就是迭代器,逐个添加到列表的末尾,返回true/false。
remove方法
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
remove有两个重载方法,一个的参数是int,表示传入index,删除指定index位置的元素,并且会返回被删除元素的值。另一个的参数是Object,表示要删除数组里的指定对象。
对于第一个传入int参数的remove方法,首先依然是判断index是否越界,如果没有,再计算numMoved,表示需要移动的元素个数。如果numMoved为0,说明删除的是末尾的元素,此时就无需复制和移动数组,直接把最后一个元素置为null即可。但如果大于0,就执行System.arraycopy(elementData, index+1, elementData, index, numMoved);
,把从index + 1开始的元素复制到index开始的位置,因此原本index位置的元素就被覆盖掉了。无论是否进行arraycopy,最后都要把最后一个元素置为null,并且修改size的值:elementData[--size] = null;
。
最后一步是否可以换成size--
?实际上是可行的,因为ArrayList里的操作都要受到size的限制,哪怕是再次调用remove,arraycopy即使多复制一个null值也不会出错,因为只要确保前面size个元素正确即可,边界判断方法确保不会访问到size之外的元素。所以这里要主动设置为null,果然就是跟注释里说的一样:提示GC这一个元素可以被回收了。
而第二个传入Object参数的remove方法,它会先判断Object是否为null,分别考虑参数是null和非null的两种情况,避免equals出错。如果找到了匹配的Object值,调用的是fastRemove方法,而这个fastRemove方法和remove方法区别不大,只是无需考虑index越界的问题,所以还省去了一个rangeCheck方法的调用,并且无需返回oldValue,仅此而已。
如图演示:
①删除index 5位置的元素
②复制index 5 + 1之后的元素到index 5(此时index 9的位置实际上还是9)
③把最后一个位置的元素设置为null,help GC。
可以看到remove也需要调用System.arraycopy,需要进行复制和移动数组,因此效率也不高。但如果remove的位置靠近结尾,那么效率尚可。
clear方法
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
没什么特别,直接全部设置为null,size设置为0,唯一值得记录的估计就是modCount只会+1,与元素的数量无关~
removeRange方法
protected void removeRange(int fromIndex, int toIndex) {
modCount++;
int numMoved = size - toIndex;
System.arraycopy(elementData, toIndex, elementData, fromIndex,
numMoved);
// clear to let GC do its work
int newSize = size - (toIndex-fromIndex);
for (int i = newSize; i < size; i++) {
elementData[i] = null;
}
size = newSize;
}
其实逻辑和remove是差不多的,但当时在源码里不小心点到了AbstractList,那里的removeRange方法看起来就比较奇怪:
protected void removeRange(int fromIndex, int toIndex) {
ListIterator<E> it = listIterator(fromIndex);
for (int i=0, n=toIndex-fromIndex; i<n; i++) {
it.next();
it.remove();
}
}
里面使用了ListIterator,自定义了一个cursor变量,所以并不是想象中的移动多次数组。此处略。
clone方法
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
先调用Object的clone方法,返回一个Object对象,再强制类型转换成ArrayList类型,接着通过Arrays.copyOf对elementData进行赋值,同时还把modCount设置为0,返回即可。
indexOf方法
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
contains方法检测ArrayList是否包含某个Object,通过indexOf方法,根据返回值是否为-1判断。而indexOf返回的是指定Object对象在ArrayList中第一次出现的位置,lastIndexOf则是最后一次出现的位置。二者都是先判断一下Object参数为null的情况,对于null参数使用“==”运算符,否则用equals方法。
get和set方法
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
很简单,没什么好分析的。
ensureCapacityInternal和grow(扩容相关)
回到add方法,在执行add操作之前,会首先调用ensureCapacityInternal方法判断是否要扩容:
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
calculateCapacity方法的意义是,如果elementData数组为DEFAULTCAPACITY_EMPTY_ELEMENTDATA,也就是创建ArrayList时调用的是无参构造方法,后面第一次add会进行扩容。此时不是从0开始一步一步地变成原本的1.5倍,而是直接创建为DEFAULT_CAPACITY(10)的大小。
这时候就可以解决构造方法时留下的疑问了,DEFAULTCAPACITY_EMPTY_ELEMENTDATA
和EMPTY_ELEMENTDATA
的区别?但调用ArrayList()时,第一次add进行扩容会直接把大小设置为10,而调用ArrayList(0),第一次add会慢慢地扩容,从1开始。可以用反射来测试:
public static void main(String[] args) {
printDefaultCapacityList();
printEmptyCapacityList();
}
public static void printDefaultCapacityList() {
ArrayList defaultCapacity = new ArrayList();
System.out.println(
"default 初始化长度:" + getCapacity(defaultCapacity));
defaultCapacity.add(1);
System.out.println(
"default add 之后 长度:" + getCapacity(defaultCapacity));
}
public static void printEmptyCapacityList() {
ArrayList emptyCapacity = new ArrayList(0);
System.out.println(
"empty 初始化长度:" + getCapacity(emptyCapacity));
emptyCapacity.add(1);
System.out.println(
"empty add 之后 长度:" + getCapacity(emptyCapacity));
}
public static int getCapacity(ArrayList<?> arrayList) {
Class<ArrayList> arrayListClass = ArrayList.class;
try {
// 获取 elementData 字段
Field field = arrayListClass.getDeclaredField("elementData");
// 开启访问权限
field.setAccessible(true);
// 把示例传入get,获取实例字段elementData的值
Object[] objects = (Object[]) field.get(arrayList);
//返回当前ArrayList实例的容量值
return objects.length;
} catch (Exception e) {
e.printStackTrace();
return -1;
}
}
运行结果:
default 初始化长度:0
default add 之后 长度:10
empty 初始化长度:0
empty add 之后 长度:1
那么讨论完DEFAULTCAPACITY_EMPTY_ELEMENTDATA的情况后,如果elementData的长度确实不足以存储minCapacity数量的元素,此时会调用grow方法进行扩容。通过int newCapacity = oldCapacity + (oldCapacity >> 1);
使得容量会是旧容量的1.5倍。如果1.5倍之后仍然没有达到minCapacity,那么直接将新容量设置为minCapacity:newCapacity = minCapacity;
实际上这一行代码只有在EMPTY_ELEMENTDATA的时候才会执行。过去总以为如果一次性往ArrayList里添加多个元素(比如addAll)会只执行1次扩容,但看了addAll方法就会发现,addAll只是通过迭代器,按顺序地对每一个元素执行add方法。此处也可以用反射进行测试,略。
序列化和反序列化
序列化writeObject和反序列化readObject:
transient Object[] elementData; // non-private to simplify nested class access
// ...
/**
* 将ArrayLisy实例的状态保存到一个流里面
*/
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// 按照顺序写入所有的元素
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
/**
* 根据一个流(参数)重新生成一个ArrayList
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt();
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
ArrayList里的elementData对象声明为transient,原因是JDK不想让整个elementData都序列化或反序列化,而是只将size和实际存储的元素进行序列化或反序列化,从而节省空间和时间。
迭代器
ArrayList因为实现了Collection,自然也实现了Iterable接口,因此可以调用iterator方法生存一个迭代器:
public Iterator<E> iterator() {
return new Itr();
}
/**
* An optimized version of AbstractList.Itr
*/
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
Itr() {}
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
代码有点长,先看三个变量:
cursor:记录迭代器当前指向的位置,游标。
lastRet:记录上一次返回的元素位置。
exceptedModCount:期望的modCount值
对于next方法,主要的逻辑是先判断各种异常情况,然后把cursor指向下一个位置,返回cursor先前指向的位置,所以实际上next方法只用看这两行:
// ...
cursor = i + 1; // 游标指向i + 1,即下一个位置
return (E) elementData[lastRet = i]; // 返回i位置的元素,即游标先前指向的位置
而remove方法,此时会删除lastRet位置上的元素,然后把cursor指向lastRet,因为数据量减1,cursor也需要减1。并且会把lastRet设置为-1,所以不能连续两次进行remove方法。这是代码角度的限制,而实际含义则是迭代器就不应该有连续删除的功能,而是逐步遍历,找到合适的才会考虑删除。
而且next和remove方法的开头都会调用checkForComodification
方法,判断modCount是否与exceptedModCount相等。因为ArrayList的迭代器是fast-fail的,因此在使用的过程中不允许对数据进行修改。此时如果修改了数据,那么modCount也会发生改变,导致modCount与exceptedModCount不相等,抛出ConcurrentModificationException。但有的时候modCount不一定会变化,这时候就不会抛出异常。所以这个异常只能用于测试,但不能因为没有抛出异常就断定一定没有在中途修改数据,关键是要清晰为什么不能修改。
通过上面的代码,我们会发现迭代器里的remove还会把exceptedModCount设置为modCount,因此这时候就不会抛出异常。所以在ArrayList上进行remove/add/set等操作会抛出异常,但在迭代器上调用remove并不会:
public static void main(String[] args) {
ArrayList arrayList = new ArrayList();
for (int i = 0; i < 10; i++) {
arrayList.add(i);
}
remove(arrayList);
System.out.println(arrayList);
}
public static void remove(ArrayList<Integer> list) {
Iterator<Integer> iterator = list.iterator();
while (iterator.hasNext()) {
Integer number = iterator.next();
if (number % 2 == 0) {
// 抛出ConcurrentModificationException异常
list.remove(number); // 如果换成iterator.remove()则不会抛出异常
}
}
}
最后关于迭代器这一块,还有一个forEachRemaining的方法,暂时没用到,先不管了。
其他
Question:ArrayList适用的场景?
Ans:ArrayList适用于需要频繁地随机查询,值的修改,在尾部添加删除元素的情况,或者元素的个数是确定的。所以ArrayList可以用于实现栈,只对最后一个元素进行操作(但LinkedList的方法名更直观)。ArrayList不适合用于实现队列,因为队列需要在头跟尾进行操作,这平均下来会需要进行大量的数组复制和移动操作,效率很低。
Question:ArrayList不适合实现队列,那数组是否适合实现队列?
Ans:实际上ArrayList跟数组基本是同一个东西,但有一种特殊的数组:环形数组。也就是数组的头尾是连接在一起的,根据两个index指向数组的头和尾(实际上一个是读的地点,一个是写的地点),并且确保不会超出范围。因此环形数组可以用于实现环形队列,比如ArrayBlockingQueue,它是一个定长的队列,底层使用一个定长的数组来实现。
Question:ArrayList和LinkedList的遍历性能差异?
Ans:论遍历ArrayList要比LinkedList快得多,ArrayList遍历最大的优势在于内存的连续性,CPU的内部缓存结构会缓存连续的内存片段,可以大幅降低读取内存的性能开销。