hashmap的滥用

具体滥用与否视你的项目而定

对于答主这样的内存敏感人士,一般不用java.util.HashMap

内存篇
以下举出一个例子
这个例子是以int为key,int为value的map,对比多种实现
说明java.util.HashMap占用内存相比其他数据结构或者hash map实现有时是不可接受的

package tmptest;

import static javax.lang.Sizeof.sizeof; // 我的常用工具,有空补上代码
import static java.lang.System.out;

public class RamCmp {

    static void printSize(Object o) {
        out.printf("类型:%s,占用内存:%.2f MB\n", o.getClass().getSimpleName(), sizeof(o) / 1024D / 1024D);
    }

    public static void main(String[] args) throws Throwable {
        int size = 30000;

        java.util.Map<Object, Object> javaUtilHashMap = new java.util.HashMap<>();
        for (int i = 0; i < size; javaUtilHashMap.put(i, i), i++) {
        }

        net.openhft.koloboke.collect.map.hash.HashIntIntMap openHftHashIntIntMap =
                net.openhft.koloboke.collect.map.hash.HashIntIntMaps.newUpdatableMap();
        for (int i = 0; i < size; openHftHashIntIntMap.put(i, i), i++) {
        }

        java.util.ArrayList<Object> javaUtilArrayList = new java.util.ArrayList<>();
        for (int i = 0; i < size; javaUtilArrayList.add(i), i++) {
        }

        Integer[] objectArray = new Integer[size];
        for (int i = 0; i < size; objectArray[i] = i, i++) {
        }

        com.carrotsearch.hppc.IntArrayList hppcArrayList = new com.carrotsearch.hppc.IntArrayList();
        for (int i = 0; i < size; hppcArrayList.add(i), i++) {
        }

        int[] primitiveArray = new int[size];
        for (int i = 0; i < size; primitiveArray[i] = i, i++) {
        }

        out.println("java.vm.name=" + System.getProperty("java.vm.name"));
        out.println("java.vm.version=" + System.getProperty("java.vm.version"));
        out.println("容器元素总数:" + size);

        printSize(javaUtilHashMap);
        printSize(openHftHashIntIntMap);
        printSize(javaUtilArrayList);
        printSize(hppcArrayList);
        printSize(primitiveArray);
        printSize(objectArray);
    }
}


输出结果如下:
java.vm.name=Java HotSpot(TM) 64-Bit Server VM
java.vm.version=25.20-b23
容器元素总数:30000
类型:HashMap,占用内存:2.08 MB
类型:UpdatableLHashParallelKVIntIntMap,占用内存:0.50 MB
类型:ArrayList,占用内存:0.58 MB
类型:IntArrayList,占用内存:0.17 MB
类型:int[],占用内存:0.11 MB
类型:Integer[],占用内存:0.57 MB


如果想验证以上结果,可以使用以下包和我的javax.lang.Sizeof工具类(有空补上)
	<dependency>
		<groupId>com.carrotsearch</groupId>
		<artifactId>hppc</artifactId>
		<version>0.5.4</version>
	</dependency>

	<dependency>
		<groupId>net.openhft</groupId>
		<artifactId>koloboke-api-jdk8</artifactId>
		<version>0.6.5</version>
	</dependency>

	<dependency>
		<groupId>net.openhft</groupId>
		<artifactId>koloboke-impl-jdk8</artifactId>
		<version>0.6.5</version>
	</dependency>

对比结果分析:

元素数量在30000个的时候,java.util.HashMap相比其他几个实现,都已经多浪费1MB内存了
甚至是最节省内存的int[]的20倍

原因是:
1. hash中为避免退化为数组(如openhft的实现可以退化为数组)或者链表(java.util.HashMap可能退化为链表)使用的空槽
2. java.util.HashMap.Entry的额外占用的内存,用于维持链表、内存对齐等
3. 对象内存占用:在HotSpot 64位jdk中,一个java.lang.Integer占用16字节,一个引用占用4字节,总共20字节,而一个int只占用4字节

设想一下,如果一个分布式系统中,java.util.HashMap的对象占用了你的大部分内存,可以想象,集群规模达到一定程度,浪费的内存的价格要比程序员的工资多得多,就有必要优化了(以上作为反驳匿名用户的答案——内存太便宜不用操心,的论据)

CPU篇

2015-04-19更新

这个比较复杂,需要分很多情况,比如:

可以把操作分为:get/put,有的map 的get更快,有的map的put更快
可以把put操作分为:已有key更新value/添加新key
可以把get操作分为:已存在的key/不存在的key
把map分为:只读 / 可添加不可删除 / 可添加可删除
可以把map元素密度分为:密集型/稀疏型
按线程分为:put&get之间可否并发,get&get之间可否并发

来看一种简单的情况:非并发,put添加新key(它首先需要完成一次get操作:查找不存在的key)
import static java.lang.Math.*;
import static java.lang.System.*;
import static java.util.Arrays.*;

import java.util.Collections;

public class CpuCmp {
    static void printTime(Class type, Runnable r) {
        double time = timeCall(r, 30);
        char[] rpad = "                                    ".toCharArray();
        type.getSimpleName().getChars(0, type.getSimpleName().length(), rpad, 0);        
        out.printf("类型:%s \t 耗时:%.2g s\n", new String(rpad), time);
    }

    public static <T> double timeCall(Runnable call) {
        long startA = nanoTime();
        long start = nanoTime();
        try {
            call.run();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return 1E-9d * (max(0, nanoTime() - start - (start - startA))); // 减去 nanoTime() 本身的耗时;
    }

    public static <T> double timeCall(Runnable call, int repeat) {
        double[] a = new double[repeat];
        setAll(a, i -> timeCall(call));
        if (repeat > 7) { // 对排序中间的 60% 计算平均
            sort(a);
            int i = round(repeat * 0.2f);
            return stream(a, i, repeat - i).average().getAsDouble();
        }
        if (repeat > 3) {
            // 去掉一个最高分,一个最低分,剩下的取平均
            sort(a);
            return stream(a, 1, repeat - 1).average().getAsDouble();
        }
        return stream(a).average().getAsDouble();
    }

    public static void main(String[] args) throws Throwable {

        int size = 1000000;

        out.println("java.vm.name=" + System.getProperty("java.vm.name"));
        out.println("java.vm.version=" + System.getProperty("java.vm.version"));
        out.println("容器元素总数:" + size);

        printTime(java.util.HashMap.class, () -> {
            java.util.Map<Object, Object> javaUtilHashMap = new java.util.HashMap<>();
            for (int i = 0; i < size; javaUtilHashMap.put(i, i), i++) {
            }
        });

        printTime(java.util.LinkedHashMap.class, () -> {
            java.util.Map<Object, Object> javaUtilLinkedHashMap = new java.util.LinkedHashMap<>();
            for (int i = 0; i < size; javaUtilLinkedHashMap.put(i, i), i++) {
            }
        });


        printTime(java.util.concurrent.ConcurrentHashMap.class, () -> {
            java.util.Map<Object, Object> javaUtilLinkedHashMap = new java.util.concurrent.ConcurrentHashMap<>();
            for (int i = 0; i < size; javaUtilLinkedHashMap.put(i, i), i++) {
            }
        });
        

        printTime(Collections.synchronizedMap(new java.util.concurrent.ConcurrentHashMap()).getClass(), () -> {
            java.util.Map<Object, Object> javaUtilLinkedHashMap = Collections.synchronizedMap(new java.util.concurrent.ConcurrentHashMap());
            for (int i = 0; i < size; javaUtilLinkedHashMap.put(i, i), i++) {
            }
        });

        printTime(java.util.TreeMap.class, () -> {
            java.util.Map<Object, Object> javaUtilTreeMap = new java.util.TreeMap<>();
            for (int i = 0; i < size; javaUtilTreeMap.put(i, i), i++) {
            }
        });

        printTime(net.openhft.koloboke.collect.map.hash.HashIntIntMaps.newUpdatableMap().getClass(), () -> {
            net.openhft.koloboke.collect.map.hash.HashIntIntMap openHftHashIntIntMap =
                    net.openhft.koloboke.collect.map.hash.HashIntIntMaps.newUpdatableMap();
            for (int i = 0; i < size; openHftHashIntIntMap.put(i, i), i++) {
            }
        });

        printTime(java.util.ArrayList.class, () -> {
            java.util.ArrayList<Object> javaUtilArrayList = new java.util.ArrayList<>();
            for (int i = 0; i < size; javaUtilArrayList.add(i), i++) {
            }
        });

        printTime(Integer[].class, () -> {
            Integer[] objectArray = new Integer[size];
            for (int i = 0; i < size; objectArray[i] = i, i++) {
            }
        });

        printTime(com.carrotsearch.hppc.IntArrayList.class, () -> {
            com.carrotsearch.hppc.IntArrayList hppcArrayList = new com.carrotsearch.hppc.IntArrayList();
            for (int i = 0; i < size; hppcArrayList.add(i), i++) {
            }
        });

        printTime(int[].class, () -> {
            int[] primitiveArray = new int[size];
            for (int i = 0; i < size; primitiveArray[i] = i, i++) {
            }
        });


    }
}

输出结果:

java.vm.name=Java HotSpot(TM) 64-Bit Server VM
java.vm.version=25.20-b23
容器元素总数:1000000
类型:HashMap                              	 耗时:0.058 s
类型:LinkedHashMap                        	 耗时:0.032 s
类型:ConcurrentHashMap                    	 耗时:0.091 s
类型:SynchronizedMap                      	 耗时:0.090 s
类型:TreeMap                              	 耗时:0.23 s
类型:UpdatableLHashParallelKVIntIntMap    	 耗时:0.054 s
类型:ArrayList                            	 耗时:0.0073 s
类型:Integer[]                            	 耗时:0.0033 s
类型:IntArrayList                         	 耗时:0.0042 s
类型:int[]                                	 耗时:0.00098 s

惊奇的发现java8中的HashMap并不比openhft的UpdatableLHashParallelKVIntIntMap慢,记忆中java6中的Map是比openhft慢很多的,我们来看下源码找下原因:
/*
 * Copyright (c) 1997, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */

也许这就是根本原因吧,Java在Oracle旗下活得不错,接下来看看直接原因:put方法实现

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    /**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    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)
            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))))
                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) {
                        p.next = newNode(hash, key, value, null);
                        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;
    }

有个Java6版HashMap没有的亮点:

计算槽位采用:
tab[i = (n - 1) & hash]
为什么不用Java6中的写法:
tab[i = hash % n] 

如果你学过《计算机组成原理》,你可以看出来%操作需要用到除法器,需要多个时钟周期,这个比&位运算这种只需要1个时钟周期的操作慢得多,在有些CPU上甚至可以慢几十倍

那为什么 hash & (n - 1) 等价于 hash % n呢,回忆一下二进制,当n是2的整数次方的时候,n-1用二进制表示形如:0000000011111111 (0*1*),和这种数字做与运算,就是保留低位,这样就和取模有一样的结果,保证n必须是2的整数次方,在resize方法中可以看到:
newCap = oldCap << 1

n一开始是2的整数次方16,每次resize,左移1位,相当于乘以2,保证了n是2的整数次方

这个实现和Java6一样也使用链表结构将相同hash的key-value节点串起来

链表有什么缺点呢?链表中的每一个节点,存储在内存条的不同物理位置上,这意味着对内存的访问是随机性的,而不是连续性的,而对内存条的随机访问的低效的(管内存条叫做Random Access Memory,真的好吗?)

为什么对内存的随机访问的低效的?CPU从内存条中读数据,需要通过L2/L3 cache,如果cache能够命中所需要的数据,就不再从内存载入,否则L2/L3 cache是一次性将一片连续的内存(比如:64字节)载入,再将计算单元用到的部分交给计算单元,而这两种操作速度有10倍的差距,我的i7 CPU,L3 cache的只有区区4MB,只能保存内存中数据的极小一部分,所以如果对内存访问不是连续的,就需要频繁的换入换出内存数据

来把容器元素数量提高10倍,来验证这个缺点:
java.vm.name=Java HotSpot(TM) 64-Bit Server VM
java.vm.version=25.20-b23
容器元素总数:10000000
类型:HashMap                              	 耗时:5.1 s
类型:LinkedHashMap                        	 耗时:7.5 s
类型:ConcurrentHashMap                    	 耗时:7.4 s
类型:SynchronizedMap                      	 耗时:5.0 s
类型:TreeMap                              	 耗时:8.9 s
类型:UpdatableLHashParallelKVIntIntMap    	 耗时:0.80 s
类型:ArrayList                            	 耗时:0.78 s
类型:Integer[]                            	 耗时:0.30 s
类型:IntArrayList                         	 耗时:0.058 s
类型:int[]                                	 耗时:0.013 s

重点是:
容器元素总数:10000000
类型:HashMap                              	 耗时:5.1 s
类型:UpdatableLHashParallelKVIntIntMap    	 耗时:0.80 s

对比之前的效果:
容器元素总数:1000000
类型:HashMap                              	 耗时:0.058 s
类型:UpdatableLHashParallelKVIntIntMap    	 耗时:0.054 s

HashMap耗时增加了100倍!!openhft的UpdatableLHashParallelKVIntIntMap只增加了13倍

为什么?来看看openhft的实现:
    public int put(int key, int value) {
        int free;
        if (key == (free = freeValue)) {
            free = changeFree();
        }
        long[] tab = table;
        int capacityMask, index;
        int cur;
        long entry;
        if ((cur = (int) (entry = tab[index = ParallelKVIntKeyMixing.mix(key) & (capacityMask = tab.length - 1)])) == free) {
            // key is absent
            incrementModCount();
            tab[index] = ((((long) key) & INT_MASK) | (((long) value) << 32));
            postInsertHook();
            return defaultValue();
        } else {
            keyPresent:
            if (cur != key) {
                while (true) {
                    if ((cur = (int) (entry = tab[(index = (index - 1) & capacityMask)])) == free) {
                        // key is absent
                        incrementModCount();
                        tab[index] = ((((long) key) & INT_MASK) | (((long) value) << 32));
                        postInsertHook();
                        return defaultValue();
                    } else if (cur == key) {
                        break keyPresent;
                    }
                }
            }
            // key is present
            int prevValue = (int) (entry >>> 32);
            U.putInt(tab, LONG_BASE + INT_VALUE_OFFSET + (((long) (index)) << LONG_SCALE_SHIFT), value);
            return prevValue;
        }
    }

有以下亮点是Java8的HashMap不具备的:

1. 用一个long[]数组存储所有节点,每个long表示一个节点,高位表示key,低位表示value,key和value的物理地址连续,而且key和value不是引用类型(指针类型),这意味不需要跳转到其他位置,避免了随机访问,增加了cache命中率
long[] tab = table;
...
tab[index] = ((((long) key) & INT_MASK) | (((long) value) << 32));

2.相同hash的不同节点之间,不采用链式结构,而采用开发寻址(OpenAddressing)
while (true) {
    ... tab[(index = (index - 1) & capacityMask)]))  ...

index = (index - 1) & capacityMask,其中capacityMask就是tab.length - 1,index每次挪动一个位置,对tab数组的内存是连续访问

对比Java8版HashMap的链式结构:
for (; ;) {
    ...  e = p.next ...

链表的缺点前文说过了,对内存是随机访问


最后,做为数组脑残粉,推广下数组大法有多好吧:
1. 你所知道的高效数据结构,多数需要依赖数组,因为你想连续访问内存只能用数组
2. 数组既是一个完美的hash map,又是一个深度仅为1的多叉树
3. 如果key是int,数组存key不需要任何空间,只需要存value,其它数据结构做不到
4. 数组是内存条的最直接表示,类实例/结构体,可以看做一个特殊的数组
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值