11. Java语言性能技术

熟悉Java Collections API中提供的数据结构的细节对开发人员非常重要


在为产品编码实现某个通用算法之前,先检查java.lang.Colections中有没有可以利用的


本文快速结论

  1. 1. 除非要利用LinkedList关键特性(插入时间为O(1)),否则建议使用ArrayList
  2. 2. ArrayList在初始化时要指定其大小

11.1 优化集合

大部分编程语言至少会提供两种通用容器

  • • 顺序容器--将对象保存在特定位置,用索引来访问
  • • 关联容器--使用对象本身来确认他在集合中的位置

为使特定容器可以正常工作,对象必须提供比较和相等的概念,即实现hashCodeequals方法。

引用类型的字段作为引用是存储在堆中的,容器中存储的不是对象本身,而是对象的引用

所以JAVA现在无法做到与C的结构体等价的数据存储

Java Collections API 的类层次结构如下图:

java性能优化实战pdf下载 java性能优化实践pdf_java


Java Collections API

11.2 针对列表的优化考虑

主要考虑ArrayListLinkedList

Stack和Vector不大必要,如果 有使用 Vector,应该考虑替换掉。

拐言,个人感觉Stack还是有用的。

11.2.1 ArrayList

  • • 在不指定size的情况下,第一次增加时会分配一个容量为10的数组.

相关源码如下

private void add(E e, Object[] elementData, int s) {
    if (s == elementData.length)
        elementData = grow();
    elementData[s] = e;
    size = s + 1;
}
private Object[] grow() {
    return grow(size + 1);
}

private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { 
        //DEFAULTCAPACITY_EMPTY_ELEMENTDATA 为长度为0的数组
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                minCapacity - oldCapacity, /* minimum growth */
                oldCapacity >> 1           /* preferred growth */);
        return elementData = Arrays.copyOf(elementData, newCapacity);
    } else {
        //DEFAULT_CAPACITY -> 10 
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}

优化方式

目的是避免反复的重新分配和copy

尽可能地设置容量,避免动态调整大小,会带来性能损失

  1. 1. 在初始化时指定size
  2. 2. 通过ensureCapacity保证容量

ensureCapacity

public void ensureCapacity(int minCapacity) {
        if (minCapacity > elementData.length
            && !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
                 && minCapacity <= DEFAULT_CAPACITY)) {
            modCount++;
            grow(minCapacity);
        }
    }

相应基准测试的代码:

@Benchmark
public List<String> properlySizedArrayList() {
    List<String> list = new ArrayList<>(1_000_000);
    for(int i=0; i < 1_000_000; i++) {
        list.add(item);
    }
    return list;
}

@Benchmark
public List<String> resizingArrayList() {
    List<String> list = new ArrayList<>();
    for(int i=0; i < 1_000_000; i++) {
        list.add(item);
    }
    return list;
}

其测试结果如下:

Benchmark                             Mode  Cnt    Score     Error  Units
ResizingList.properlySizedArrayList  thrpt   10  287.388  ± 7.135   ops/s
ResizingList.resizingArrayList       thrpt   10  189.510  ± 4.530   ops/s

11.2.2 LinkedList

  • • 双向链表
  • • 插入性能总是O(1)

java性能优化实战pdf下载 java性能优化实践pdf_链表_02


LinkedList的结构

11.2.3 ArrayList 与 LinkedList的对比

插入性能及访问性能对比

Benchmark                     Mode  Cnt    Score    Error   Units
InsertBegin.beginArrayList   thrpt   10    3.402 ±  0.239  ops/ms
InsertBegin.beginLinkedList  thrpt   10  559.570 ± 68.629  ops/ms
AccessingList.accessArrayList   thrpt   10  269568.627 ± 12972.927  ops/ms
AccessingList.accessLinkedList  thrpt   10       0.863 ±     0.030  ops/ms

工程实践

  1. 1. 除非要利用LinkedList关键特性(插入时间为O(1)),否则建议使用ArrayList
  2. 2. ArrayList在初始化时要指定其大小

11.3 针对映射的优化考虑

  • • 映射通常指键值对应关系(K-V)
  • • 在Java里,都遵循java.util.Map<K,V>接口
  • • 键与值都是引用类型

11.3.1 HashMap

HaspMap的结构

java性能优化实战pdf下载 java性能优化实践pdf_java_03


HashMap结构

  • • 在HashMap中,当key的HashCode一值,也就是冲突时,默认以链表进行处理
  • • 当链表长度大于8时,会将链表转成红黑树
  • • HashMap中一个重要值为initialCapacity,表示默认的桶数,默认为16
  • • 对桶进行增加的过程是原容量*2
  • • HashMap中另一个重要值是loadFactor,表示散列达到多满时会进行再散列(扩容)
  • • loadFactor 默认为 0.75
  • • 在知道K的数量的情况下,可以将initialCapacity设置成Count(k)/loadFactor

HashMap关键代码:

public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(key)) == null ? null : e.value;
 }

 /**
  * Implements Map.get and related methods.
  *
  * @param key the key
  * @return the node, or null if none
  */
 final Node<K,V> getNode(Object key) {
     Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
     if ((tab = table) != null && (n = tab.length) > 0 &&
         (first = tab[(n - 1) & (hash = hash(key))]) != null) {
            // 不为空 && 长度>0 
         if (first.hash == hash && // 检验第一个node
             ((k = first.key) == key || (key != null && key.equals(k))))
             return first;
         if ((e = first.next) != null) {
             //如果是树形结构,则访问树
             if (first instanceof TreeNode)
                 return ((TreeNode<K,V>)first).getTreeNode(hash, key);
             //仍为数组结构的情况
             do {
                 if (e.hash == hash &&
                     ((k = e.key) == key || (key != null && key.equals(k))))
                     return e;
             } while ((e = e.next) != null);
         }
     }
     return null;
 }
 static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                 boolean evict) {
      Node<K,V>[] tab; Node<K,V> p; int n, i;
      if ((tab = table) == null || (n = tab.length) == 0)
          n = (tab = resize()).length;
      if ((p = tab[i = (n - 1) & hash]) == null)
          //初始化或第一次有相应hash值的情况,用hash&n-1 得到第一次hash的位置
          tab[i] = newNode(hash, key, value, null);
      else {
          Node<K,V> e; K k;
          if (p.hash == hash &&
              ((k = p.key) == key || (key != null && key.equals(k))))
              // 在第一个if已经对p进行了赋值,此时p不为空,得了当前的结点 
              e = p;
          else if (p instanceof TreeNode)
              // 得到的结点已经是树结点,调用红黑树的逻辑添加
              e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
          else {
              //得到的结点是链表
              for (int binCount = 0; ; ++binCount) {
                  if ((e = p.next) == null) {
                      //在链表的最后一个增加相应值,此时链表长度为binCount
                      p.next = newNode(hash, key, value, null);
                      //如果当前值比TREEIFY_THRESHOLD(为常量8)大,则转成红黑树
                      if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                          treeifyBin(tab, hash);
                      break;
                  }
                  if (e.hash == hash &&
                      ((k = e.key) == key || (key != null && key.equals(k))))
                      break;
                  p = e;
              }
          }
          if (e != null) { // existing mapping for key
              V oldValue = e.value;
              if (!onlyIfAbsent || oldValue == null)
                  e.value = value;
              // 虚方法
              afterNodeAccess(e);
              return oldValue;
          }
      }
      ++modCount;
      // 如果大指定容量,则进行扩容
      if (++size > threshold)
          resize();
      // // 虚方法 
      afterNodeInsertion(evict);
      return null;
  }

LinkedHashMap

是HashMap的一个子类,用双向链表来存储桶 真正需要用到的场合并不多。

11.3.2 TreeMap

TreeMap 本质是一种红黑树实现

因为TreeMap的Key需要排序,所以需要K实现equals()

TreeMap 的 put,get,containsKey和remove操作的性能都为log(n)

  • • 大多数情况用HashMap已经足够
  • • 使用流或Lamda处理Map中的内容时,使用TreeMap更好。

即涉及到要进行Key遍历的情况,推荐TreeMap

11.3.3 缺少MultiMap

  • • Java没有提供MutilMap(一个键对多个值的实现)
  • • 可以通过Map<K,List<V>> 处理。

11.4 针对集合的优化考虑

  • • JDK 默认提供三种Set,HashSet,LinkedHashSet,TreeSet
  • • Set的实现思路其实与Map一致
  • • 按照对MAP优化的思路优化Set

11.5 领域对象

  • • 领域对象容易引起内存泄露
  • • Java堆分配的多为客串,字符数组,字节数组和集合
  • • 大得反常的数据集有可能引起内存泄露


11.6 避免终结化

  • • Java 用 finalize()尝试自动管理。
  • • 当一个类型(文件句柄或TCP连接)所有者是一个Obj时,资源的释放变成平台责任,而与程序员无关。
  • • 似乎没有办法确认finalize()的执行时机。

11.6.1 血泪史 忘记清理



  1. 1. 有请求进入时,建立一个TCP连接;
  2. 2. 异常发生时,没有关闭连接;
  3. 3. 然后就,内存溢出。。。

11.6.2 为何不用终结化

关于finalize

当垃圾收集器确定没有更多引用指向该对象时,调用该方法。子类通过覆盖来释放系统资源或执行其他清理;

以上:

  1. 1. 只有垃圾清理时才会调用finalize
  2. 2. 如果finalize()抛出异常,开发人员处理不了(因为是GC执行了finalize())
  3. 3. finalize()执行时间未知,不建议使用

如果一个对象实现了finalize(),在回收时:

  1. 1. 它被移到一个队列中;
  2. 2. 应用重启后,有专门的线程处理该队列,依次运行finalize()
  3. 3. finalize()结束后,对象在下一个周期回收

11.6.3 Try-With-Resource

可以用try-with-resource实现资源自动清理.

public void readFirstLineNew(File file) throws IOException {
        try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
            String firstLine = reader.readLine();
            System.out.println(firstLine);
        }
    }

11.7 方法句柄

  1. 1. 方法句柄是Java7引入的关键特性(java.lang.invoke.MehtodHandle)
  2. 2. 方法句柄表示直接可执行的引用
  3. 3. 方法句柄可以最大限度保持线程安全


IT老拐瘦