我們熟悉的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次,儘可能在減少停頓時間的情況下最大化的回收垃圾。