一、概述
首先我们再次熟悉hashmap存储过程。
当添加一个元素(key-value)时,首先计算key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,就形成了链表,同一个链表上的Hash值是相同的,所以说数组存放的是链表。 JDK8中,当链表长度大于8时,链表就转换为红黑树,这样又大大提高了查找的效率。
![](https://img.haomeiwen.com/i12709532/0669a3e1d50c2930.jpeg)
二、HashMap取数据过程
明白了存储数据过程,取数据就比较简单了,参见以下步骤:
(1) 获得key的hashcode,通过hash()散列算法得到hash值,进而定位到数组的位置。
// 根据key算出hash
int hash = (key == null) ? 0 : hash(key);
// 先算出hash在table中存储的index,然后遍历table下标为index的单链表
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
// 如果hash和key都相同,则把Entry返回
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
(2) 在链表上挨个比较key对象。 调用equals()方法,将key对象和链表上所有节点的key对象进行比较,直到碰到返回true的节点对象为止。
(3) 返回equals()为true的节点对象的value对象
三、 HashMap使用不当导致的内存泄露
从存储原理我们可以分析出,数据在数组中的存放位置,是取决于Key对象 hashCode() 方法的。一个对象的hashCode() 方法的值,一般来说都是和对象的内容相关的。那么,如果Key对象的成员取值变化了,它的hashCode() 基本上也会变化。
这样就会有问题了。
在存放进去的时候,和取出数值的时候,都是依赖Key 对象的hashCode计算的。
设想一下,一个KV对被put 到HashMap中的时候,hashCode 是A,然后这个时候修改了Key的内容,使得它的hashCode变化了;那么接下来再用 get 方法获取这个 Value 时,会用这个Key重新计算hashCode,然后去内部查找,由于hashCode已经变化了,所以这时是找不到的。
总结:Key存储对象,如果对象的值改变,hashcode也会跟着改变,再获取此对象,就获取为空。另外,HashMap内部还存在的它曾经对应的key,它会因为HashMap的持有而无法被GC回收,从而会导致内存泄漏。
四、解决方案
从以上的分析可以知道,该问题的本质原因,在一个Key被放进HashMap 和从 HashMap 取出时,两次计算它的 hashCode 不一致导致的。所以要避免这个问题,就需要保证一个 Key 在 put 和 get 时,它的hashCode 保持不变。
具体实现为:
1. 保证作为HashMap 的Key 的对象是不可变的。也就是使用只读多对象来当作Key;
2. 如果要使用一个类当作HashMap的key,同时也要修改它的内容。那么可以重写hashCode,保证hashCode 的生成只和某些不可变的成员相关。也就是说只要hashCode不变,通过hash()散列算法得到hash值,进而定位到数组的位置也不会变,如果后面再改变key的值,对存储位置不影响,只是不能获取到,不影响GC。如果Key改变,那么接下来再用 get 方法获取这个 Value 时,会用这个Key重新计算hashCode,然后去内部查找,由于hashCode已经变化了,所以这时是找不到的,存储那么这个在HashMap内部存在的它曾经对应的Node,将会导致内存泄漏。它会因为HashMap的持有而无法被GC回收,但是用HashMap却也获取不到它。
网友评论