jdk8中HashMap的优化
HashMap是基于哈希表的 Map 接口的实现。此实现提供所有可选的映射操作,并允许使用 null 值和 null 键。(除了不同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同。)此类不保证映射的顺序,特别是它不保证该顺序恒久不变。另外,HashMap是非线程安全的,也就是说在多线程的环境下,可能会存在问题,而Hashtable是线程安全的。
为什么HashMap集合在储存数据的时候要使用哈希算法?
众所周知,HaspMap中存储的数据都是不可重复的并且是无序的,那么我们在存储一个新的数据的时候就需要判断这个数据原来是否存在,这个时候就需要通过java中的equals方法来判断两个对象的实例是否相等,如果相等,那么就是重复数据。但是,如果每增加一个元素就检查一次,那么当元素比较多时,添加新元素的时候就需要用equals比较很多次,这个时候存储的效率就非常低。
例如:如果一个HashMap集合中已经有10000个元素,这个时候添加一个新元素的时候就需要用equals比较10000次,再添加一个元素,就需要用equals比较10001次,存储的元素越多,效率就会越低。
于是,Java便采用哈希算法来提高效率。当向集合中添加新的元素的时候,先将对象通过哈希算法(hashCode方法)计算得到哈希值(正整数),然后将哈希值和集合(数组)长度进行&运算,得到该对象在该数组存放位置的索引值。如果这个位置上没有元素,就可以直接存储在这个位置上,如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就覆盖,不相同的话就发生碰撞,形成链表。
(哈希算法:https://www.cnblogs.com/xiohao/p/4389672.html)
HaspMap的底层原理是如何实现的?
- initialCapacity:初始容量。指的是HashMap集合在初始化的时候自身的容量。可以在构造方法中指定;如果不指定的话,总容量默认值是16。
注意:HashMap的初始容量必须是2的幂次方 - size:当前HashMap中已经存储着的键值对数量,对应的获取方法为HashMap.size()
- loadFactor:加载因子。加载因子指的是当HashMap的容量(加载因子=当前的容量/总容量)到达一定的值的时候,HashMap会实施扩容。加载因子也可以通过构造方法来指定,默认值是0.75。
例如:一个HashMap集合的初始容量为16,那么扩容的阙值就是0.75 * 16 = 12。也就是说,在你打算存入13个值的时候,HashMap会先执行扩容,然后再进行存储。 - threshold: 扩容阙值。扩容阙值 = HashMap总容量 * 加载因子。当前HashMap的容量大于或等于扩容阙值的时候就会去执行扩容。扩容的容量为当前HashMap总容量的两倍。例如,当前HashMap的总容量为16,那么扩容之后就为32。
- table:Entry数组。HashMap内部存储key/value是通过Entry这个介质来实现的。而table就是Entry数组。
- 在jdk 1.7 中,HashMap的实现方法就是数组 + 链表的形式。上面的table就是数组,而数组中费每个元素,都是链表的第一个结点。如下图所示:
存储过程:
当向集合中添加新的元素的时候,先将对象通过哈希算法(hashCode方法)计算得到哈希值(正整数),然后将哈希值和集合(数组)长度进行&运算,得到该对象在该数组存放位置的索引值。
如果这个位置上没有元素,就可以直接存储在这个位置上,如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就覆盖,不相同的话就发生碰撞,形成链表。
碰撞我们应该要尽力去避免,如果形成的链表中的元素过多,就会降低效率,那么我们就需要通过提高hasp算法和equals方法来减少碰撞,但是无论如何肯定都会有发生碰撞的可能,只能说是减少概率,却无法避免。因为当我们添加一个元素的时候,会通过哈希算法,算出一个哈希码值,哈希码值经过运算,算出数组的索引值,但是数组的索引值是固定的,所以必然会发生碰撞,于是,为了减少碰撞,HashMsp又提供了一个加载因子,加载因子默认是0.75,这个0.75指的是当元素到达现有哈希表的75%的时候自动进行扩容哈希表,这个因子不能太大,太大的话就会影响效率,也不能太小,太小的话就会浪费空间,一旦扩容,那么HaspMap中的元素将会重新计算。
jdk8中对HashMap做了哪些改变?
- 在java 1.8中,如果链表的长度超过了8,那么链表将转换为红黑树。
- 发生hash碰撞时,java 1.7 会在链表的头部插入,而java 1.8会在链表的尾部插入
-
在java 1.8中,Entry被Node替代(换了一个马甲)。
存储过程 :
在jdk7的基础上,形成了链表之后,当我们查询一个对象的时候,如果这个位置已经形成了链表,那么此时查询的效率就比较低了,因为我们得遍历这个链表,运气不好的话,可能要查找的元素就是在链表的最后一个,那么此时我们就需要把整个链表都遍历一遍,效率比较低。
在jdk1.8之后,在数组+链表的基础上,还多了一个红黑树。现在的结构就是数组+链表+红黑树,当碰撞的次数大于8并且总容量大于64的时候,链表就会变为红黑树结构,转为红黑树之后,除了添加以外,其他的效率都比链表高,因为在添加的时候,链表是直接加到链表的末尾,而红黑树添加的时候,需要比较大小,然后再进行添加。转为红黑树之后,集合进行扩容以后,不用重新对元素进行计算,只需要找每个元素的二倍,然后把元素放入位置就可以了,提高了效率源码分析
/** * 初始容量默认值 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * 加载因子默认值 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 默认构造函数 * */ public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // 使用默认的加载因子 } /** * 指定初始化容量,加载因子使用默认加载因子 */ public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } /** * 指定初始容量和加载因子 */ public HashMap(int initialCapacity, float loadFactor) { //对初始容量的值进行判断 if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); }
/**
* put方法
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* putVal方法
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node[] tab; Node 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 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)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;
}
/**
* get方法
*/
public V get(Object key) {
Node e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* getNode方法
*/
final Node getNode(int hash, Object key) {
Node[] tab; Node first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode)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;
}
### jdk8之后,ConcurrentHashMap发生了怎么样的变化?
在jdk1.7的时候,ConcurrentHashMap有一个并发级别,默认的并发级别是16,采用的是锁分段机制,每个段中对应一个表。
在jdk1.8中,锁分段机制就没有使用了,改为了CAS算法(无锁算法),之所以取消锁分段机制,因为这个并发级别的大小不好评定,如果太大,就有可能有些段里面没有元素,造成空间浪费,太小的话,造成每个段的元素过多,效率降低。
(CAS算法:https://www.cnblogs.com/Mainz/p/3546347.html)
### jdk8对底层内存结构做了哪些优化
jdk1.8以前内存分为栈,堆,方法区,堆内存中又有永久区,而方法区也属于永久区的一部分,永久区的内容几乎不会被垃圾回收机制回收,因为被垃圾回收机制回收的条件比较苛刻。
jvm有许多种,比如Oracle-SUN的Hotspot,Oracle的JRocket,IBM的J9 JVM,阿里Taobao JVM,除了Oracle-SUN的Hotspot之外,其他的jvm早就没有永久区了,把方法区从永久区剥离出来了,jdk1.8之后,永久区也被去除了,取而代之的叫作MetaSpace 元空间,较之前的方法区不同的是,元空间是用的是物理内存,物理内存多大,元空间就有多大,虽然之前的方法区被回收的条件比较苛刻,但是也是会被回收的,比如,当方法区快要满的时候,那么垃圾回收机制就会对其进行回收
![](https://s1.51cto.com/images/blog/201904/16/ae7d7682b9d51ce6d8a3d19834ece094.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)