咱们相熟的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次,尽可能在缩小进展工夫的状况下最大化的回收垃圾。