【Java程序优化】- 深度剖析 List 性能分析

List 是重要的数据结构之一。最常用的的便是: ArrayList、Vector 和 LinkedList 三种了,他们类图如下图所示:

由上图可知,这三种 List 都实现了 Collection 和 List 接口。

这三种不同的实现中,ArrayList 和 Vector 使用了数组实现,封装了对内部数组的操作。它们俩唯一的区别是 ArrayList 没有任何一个方法是同步的,而 Vector 则是做了线程同步。我们之后均以 ArrayList 为例进行讲解。

LinkList 使用了双向链表数据结构,并且维护了 first 和 last 两个成员变量来指示链表的头和尾。这和 ArrayList 是截然不同的实现技术,也决定了它们适用于不同的场景中。

LinkedList 是由一系列表项连接而成的。一个表项总是分为三个部分,分别是:元素内容,前驱表项 和 后驱表项,如下图所示:

在 JDK 的视线中,不管 LinkedList 是否为空,链表中总是有一个 header 表项,它即表示链表的开始,也表示链表的结尾。

下面以增加和删除为例,比较 ArrayList 和 LinkList 的不同之处。

1、增加元素到列表尾端

在 ArrayList 中增加元素到队列端尾的代码如下:

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 确保内部数组有足够的空间
        elementData[size++] = e;
        return true;
    }

ArrayList 中 add() 方法的性能取决于 ensureCapacity() 方法。ensureCapacity() 的实现如下:

	/**
	 * 分配的最大内存。
	 * 有些虚拟机会为数组保留了一些头部内容,试图分配更多的空间会引起内存溢出
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    private void ensureCapacityInternal(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); //计划扩容到现在容量的1.5倍
        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); //进行新旧数组的复制
    }

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

可以看到,只要 ArrayList 的当前容量很大,add() 操作的效率是非常高的。只有当 ArrayList 对容量的需求超过当前数组的大小时,才需要扩容。扩容过程中,会进行大量的数组复制操作。当数组复制时,最终调用
Arrays.copyof() 方法。

LinkedList 的 add() 操作实现如下,它将任意元素增加到队列的尾端:

   public boolean add(E e) {
        linkLast(e);
        return true;
    }
  void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

linkLast() 方法将 e 插入到链表的末尾。可见,LinkedList 由于使用了链表的结构,因此不需要维护容量的大小。从这点上,它比 ArrayList 有一定的性能优势,然而每次元素增加都要新建一个 Entry 对象,并进行更多的赋值操作。在频繁的系统调用中,对性能有一定的影响。

分别使用 ArrayList 和 LinkedList 运行以下代码:

public class ListTest {
	public static void testArrayList() {
		Object obj = new Object();
		List list = new ArrayList<>();
		for (int i = 0; i < 500000; i++) {
			list.add(obj);
		}
	}

	public static void testLinkedList() {
		Object obj = new Object();
		List list = new LinkedList<>();
		for (int i = 0; i < 500000; i++) {
			list.add(obj);
		}
	}

	public static void main(String[] args){
		testArrayList();
		testLinkedList();
	}
}

使用 TPTP 分析运行结果为:

可以看到,ArrayList 的性能比 LinkedList 性能高出 4 倍左右。

2、增加元素到列表任意位置

在 ArrayList 中,任意位置插入的实现代码如下:

    public void add(int index, E element) {
        rangeCheckForAdd(index); //数组越界检查

        ensureCapacityInternal(size + 1);  // 增加modCount值
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index); //index 之后的元素都需要后移一个单位
        elementData[index] = element;
        size++;
    }

可以看到,每次插入操作,都会进行一次数组的复制。而这个操作在增加元素到 List 尾端的时候是不存在的。大量的数组重组操作会导致系统性能低下。并且,插入的元素在 List 中的位置越靠前,数组重组的开销也越大。

而LinkedList 此时就显示出了优势:

    public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }

可见,对于 LinkedList 来说,在 List 尾端插入数据与在任意位置插入数据是一样的。并不会因为插入的位置靠前而导致插入方法的性能降低。现在在极端情况下对他们的这个方法进行测试,每次都将元素插入到 List 的最前端。

public class ListTest {
	public static void testArrayList() {
		Object obj = new Object();
		List list = new ArrayList<>();
		for (int i = 0; i < 50000; i++) {
			list.add(0, obj);
		}
	}

	public static void testLinkedList() {
		Object obj = new Object();
		List list = new LinkedList<>();
		for (int i = 0; i < 50000; i++) {
			list.add(0, obj);
		}
	}

	public static void main(String[] args) {
		testArrayList();
		testLinkedList();
	}
}

运行以上代码,执行时间如下:

可以看出,两者性能有天差地壤的差别。

然后每次都插入中间位置:

list.add(list.size()>>1, obj);

性能结果如下:

可以看到,此时 LinkedList 的性能反而满了好几倍。

我们再插入末尾位置:

list.add(list.size(), obj);

性能结果如下:

我们可以看到,依然是 LinkedList 的性能较慢。

因此通过对比以上三种情况下的插入操作,我们得到如下结论:当插入的位置靠前的时候,ArrayList 的性能是由于 LinkedList 的。而当插入的位置越来越靠后, ArrayList 的性能变得比 LinkedList 越来越好。

这是因为: ArrayList 的性能损耗主要在于每次插入需要复制数组,LinkedList 的性能损耗则在循环遍历链表找插入位置元素的上面。当插入位置靠前的时候,ArrayList 会花费大量的时间在复制数组上面,而 LinkedList 遍历链表找插入位置的时间消耗确是最少的。随着插入位置越来越靠后, ArrayList 需要复制的数组越来越小,消耗的时间越来越少。而 LinkedList 寻找的时间越来越多(确切的说,当插入位置为中间的时候,LinkedList
寻找的时间是最多的 )或者说是比不上 ArrayList 复制数组时间的减少速度。

3、删除任意位置元素

对于 ArrayList 来说, remove() 方法和 add() 方法雷同。在任意位置移除元素后,都要进行数组的重组。实现如下:

    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; // Let gc do its work

        return oldValue;
    }

可以看到,在 ArrayList 的每一次有效地元素删除操作后,都要进行数组的重组。并且删除的位置越靠前,数组重组时的开销也越大;要删除的元素越靠后,开销越小。

对于 LinkedList ,删除操作的实现如下:

public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }
Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

主要的时间消耗也是在寻找删除位置元素上面。

删除的性能对比和增加的性能对比可以参照插入操作的性能对比。

4、容量参数

容量参数是ArrayList 和 Vector 等基于数组的 List 的特有性能参数,它表示初始化数组大小。每一次数组元素数量超过其现有大小的时候,它表会进行一次扩容,数组的扩容会导致整个数组进行一次内存复制。因此合理的数组大小有助于减少数组扩容的次数,从而提高系统性能。

默认情况下, ArrayList 的数组初始大小为10,每次进行扩容将新的数组大小设置为原来的1.5倍。

5、遍历列表

在 JDK1.5 之后,至少有三种遍历的方式:ForEach、迭代器、for循环。

package bupt.xiaoye.charpter2.list;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class TestFor {
	public static void testForEach(List list) {
		Object temp;
		for(Object t : list)
			temp = t;
	}

	public static void testFor(List list) {
		Object temp;
		for (int i = 0; i < 1000000; i++) {
			temp = list.get(i);
		}
	}
	public static void testIterator(List list) {
		Object temp;
		for(Iterator<Object> it = list.iterator();it.hasNext();){
			temp = it.next();
		}
	}

	public static void main(String[] args) {
		Object obj = new Object();
		List list = new ArrayList();
		for (int i = 0; i < 1000000; i++) {
			list.add(obj);
		}
		testFor(list);
		testForEach(list);
		testIterator(list);
	}
}

运行结果为:

可以看到,直接for循环效率最高,其次是迭代器和 ForEach操作。

作为语法糖,其实 ForEach 编译成 字节码之后,使用的是迭代器实现的,反编译后,testForEach方法如下:

	public static void testForEach(List list) {
		for (Iterator iterator = list.iterator(); iterator.hasNext();) {
			Object t = iterator.next();
			Object obj = t;
		}
	}

可以看到,只比迭代器遍历多了生成中间变量这一步,因为性能也略微下降了一些。

时间: 2024-10-07 01:27:50

【Java程序优化】- 深度剖析 List 性能分析的相关文章

Java 程序优化 (读书笔记)

--From : JAVA程序性能优化 (葛一鸣,清华大学出版社,2012/10第一版) 1. java性能调优概述 1.1 性能概述 程序性能: 执行速度,内存分配,启动时间, 负载承受能力. 性能指标: 执行时间,CPU时间,内存分配,磁盘吞吐量,网络吞吐量,响应时间. 优化策略: 木桶原理,优化性能瓶颈. 1.2 性能调优的层次 设计调优, 代码调优, JVM调优, 数据库调优, 操作系统调优. 2. 设计优化 2.1 善用设计模式 单例模式: 对于巨大对象,节省创建对象的时间空间: 代理

valgrind 打印程序调用树+进行多线程性能分析

使用valgrind的callgrind工具进行多线程性能分析 yum install valgrind / wget http://valgrind.org/downloads/valgrind-3.4.1.tar.bz2tar xvf valgrind-3.4.1.tar.bz2cd valgrind-3.4.1/./configure --prefix=/usr/local/webserver/valgrindmakemake install 简介 valgrind是开源的性能分析利器.

java性能优化笔记(三)java程序优化

程序代码优化要点: 字符串优化:分析String源码,了解String常用方法,使用StringBuffer.StringBuilder. List.Map.Set优化:分析常用ArrayList.LinkedList.HashMap.TreeMap.LinkedHashMap.Set接口.集合常用方法优化. 使用NIO:Buffered.Channel操作和原理,使用零拷贝. 引用优化:强引用.弱引用.软引用.虚引用.WeekHashMap. 优化技巧:常用代码优化技巧.这里不一一罗列,请参考

Java程序优化细节

1. 尽量在合适的场合使用单例 使用单例可以减轻加载的负担,缩短加载的时间,提高加载的效率,但并不是所有地方都适用于单例,简单来说,单例主要适用于以下三个方面:    1).控制资源的使用,通过线程同步来控制资源的并发访问;    2).控制实例的产生,以达到节约资源的目的;    3).控制数据共享,在不建立直接关联的条件下,让多个不相关的进程或线程之间实现通信. 2. 尽量避免随意使用静态变量 要知道,当某个对象被定义为stataic变量所引用,那么gc通常是不会回收这个对象所占有的内存,如

Java程序开发中的简单内存分析

首先说明内存总体分为了4个部分, 包括 1.stack segment (栈区存储基本数据类型的局部变量,对象的引用名) 2.heap segment(堆区,一般用于存储java中new 出来的对象) 3.code segment (代码段) 4.data segment (数据段,静态数据常量) 其中我们程序中用关键字new出来的东西都是存放在heap segment: 程序中的局部变量存放在stack segment,这些局部变量是在具体方法执行结束之后,系统自动释放内存资源(而heap s

程序员谈话系列————Redis性能分析

在一些网络服务的系统中,Redis 的性能,可能是比 MySQL 等硬盘数据库的性能更重要的课题.比如微博,把热点微博[1],最新的用户关系,都存储在 Redis 中,大量的查询击中 Redis,而不走 MySQL. 那么,针对 Redis 服务,我们能做哪些性能优化呢?或者说,应该避免哪些性能浪费呢? Redis 性能的基本面 在讨论优化之前,我们需要知道,Redis 服务本身就有一些特性,比如单线程运行.除非修改 Redis 的源代码,不然这些特性,就是我们思考性能优化的基本面. 那么,有哪

Java 程序优化:字符串操作、基本运算方法等优化策略(二)

五.数据定义.运算逻辑优化 多使用局部变量 调用方法时传递的参数以及在调用中创建的临时变量都保存在栈 (Stack) 里面,读写速度较快. 其他变量,如静态变量.等,都在堆实例变量 (heap) 中创建,读写速度较慢. 清单 12 所示代码演示了使用局部变量和静态变量的操作时间对比. 位运算代替乘除法 位运算(>>    <<)是所有的运算中最为高效的. 一维数组代替二维数组  JDK 很多类库是采用数组方式实现的数据存储,比如 ArrayList.Vector 等,数组的优点是随

Java程序优化

一. 字符串优化处理 1. String对象组成:char数组,offset偏移量,count长度: 2. String对象特点: 不变性:String对象一旦生成,则不能再对它进行改变: 针对常量池的优化:当两个String对象拥有相同的值时,他们只引用常量池中的同一个拷贝: 类的final定义: 3. subString() subString()方法之所以会引起内存泄漏,是因为调用了String(int offset, int count, char value[])构造函数,此构造函数采

《Java程序性能优化》学习笔记 Ⅰ设计优化

豆瓣读书:http://book.douban.com/subject/19969386/ 第一章 Java性能调优概述 1.性能的参考指标 执行时间: CPU时间: 内存分配: 磁盘吞吐量: 网络吞吐量: 响应时间: 2.木桶定律   系统的最终性能取决于系统中性能表现最差的组件,例如window系统内置的评分就是选取最低分.可能成为系统瓶颈的计算资源如,磁盘I/O,异常,数据库,锁竞争,内存等. 性能优化的几个方面,如设计优化,Java程序优化,并行程序开发及优化,JVM调优,Java性能调