我们熟悉的hash结构,首先是一个数组元素的哈希桶,比如下图,是长度为4的哈希桶。
也就是说,当key通过hash计算后,对4进行取模,根据结果存放这个指定位置。比如取模后值为0,那就放第一个位置。
哈希桶的每个位置,保存的是entry的对象,这个entry对象包括key、value以及entry对象。
这个速度是非常快的,时间复杂度是O(1),所以map的性能还是不错的。
如果模数相等呢?我们的entry对象中包括了entry对象,也就是说会通过链表,把相同模数的entry连接起来。
但是数据量越来越大的时候,就会发现一个问题,那就是相同的模数会越来越多,也就是说哈希表的冲突问题越来越严重,严重到影响到性能。
那怎么办呢?
常用的方法是通过rehash扩容来解决hash冲突,哈希桶数量的数量,让entry存放于更多的哈希桶钟。也就是上面的4变成8,变成16,这样可以直接降低冲突的概率。
看起来很完美,除了并发可能带来的死循环(参考HashMap源码解析),依然有一个潜在的风险点。
正常rehash的时候,步骤是这样的:
- 创建一个新的哈希桶,数量是之前的2倍。
- 把旧的数据重新计算存入新的哈希桶中。
- 删除旧的哈希桶。
如果数据量很大的话,那整个过程就一直堵塞在第二个步骤中。
redis
redis也用了哈希桶,他是这么解决的,把每一次的拷贝,都分摊在每一个请求中。
假设原长度为4,新长度为8。
那第一次处理请求的时候,就只处理1上面的entry链表。
此时4上面的entry链表还没处理,当第二次请求的时候,就会处理。
通过分批处理,巧妙的把一次非常耗时的操作,分摊到一个个小的操作里。(但是请求 的时候,还是要考虑两个哈希桶存在的情况)。
JVM
分批操作,在JVM的G1收集器也有类似的手法。
redis是单线程,所以需要避免耗时的rehash的操作导致线程阻塞,而JVM回收垃圾的时候,会stop the world,也会阻塞应用,所以也要避免长时间的垃圾回收操作。
所以G1是会根据停顿时间来进行垃圾回收,有时候回收的垃圾,并不能释放理想的内存空间,那就会分多次进行回收,最多8次,尽可能在减少停顿时间的情况下最大化的回收垃圾。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。