本文基於 OpenJDK 11, HotSpot 虛擬機
在開發過程中我們可能會經常接觸到hashcode
這個方法來生成哈希碼,那麼底層是如何實現的?使用時有何注意點呢?
hashcode() 方法底層實現
hashcode()
是Object
的方法:
@HotSpotIntrinsicCandidate
public native int hashCode();
它是一個native
的方法,並且被@HotSpotIntrinsicCandidate
註解修飾,證明它是一個在HotSpot中有一套高效的實現,該高效實現基於CPU指令。
具體的實現參考源碼synchronizer.cpp
:
static inline intptr_t get_next_hash(Thread* self, oop obj) {
intptr_t value = 0;
if (hashCode == 0) {
value = os::random();
} else if (hashCode == 1) {
intptr_t addr_bits = cast_from_oop<intptr_t>(obj) >> 3;
value = addr_bits ^ (addr_bits >> 5) ^ GVars.stw_random;
} else if (hashCode == 2) {
value = 1;
} else if (hashCode == 3) {
value = ++GVars.hc_sequence;
} else if (hashCode == 4) {
value = cast_from_oop<intptr_t>(obj);
} else {
unsigned t = self->_hashStateX;
t ^= (t << 11);
self->_hashStateX = self->_hashStateY;
self->_hashStateY = self->_hashStateZ;
self->_hashStateZ = self->_hashStateW;
unsigned v = self->_hashStateW;
v = (v ^ (v >> 19)) ^ (t ^ (t >> 8));
self->_hashStateW = v;
value = v;
}
value &= markWord::hash_mask;
if (value == 0) value = 0xBAD;
assert(value != markWord::no_hash, "invariant");
return value;
}
可以看出,根據hashcode
這個全局變量的取值,決定用何種策略生成哈希值,查看globals.hpp
來看是哪一種變量:
experimental(intx, hashCode, 5, "(Unstable) select hashCode generation algorithm")
發現是一個experimental
的 JVM 變量,這樣的話,想要修改,必須添加額外的參數,如下所示:
-XX:+UnlockExperimentalVMOptions -XX:hashCode=2
並且,這個hashCode
默認爲5。
哈希值是每次hashcode()方法調用重計算麼?
對於沒有覆蓋hashcode()
方法的類,實例每次調用hashcode()
方法,只有第一次計算哈希值,之後哈希值會存儲在對象頭的 標記字(MarkWord) 中。
(上圖來自於:https://www.cnblogs.com/helloworldcode/p/11914053.html)
如果進入各種鎖狀態,那麼會緩存在其他地方,一般是獲取鎖的線程裏面存儲,恢復無鎖(即釋放鎖)會改回原有的哈希值。
關於對象頭結構,以及對象存儲結構,感興趣的話,可以參考:Java GC詳解 - 1. 理解Java對象結構
-XX:hashCode=0 利用 Park-Miller 僞隨機數生成器生成哈希值
if (hashCode == 0) {
value = os::random();
}
調用 os 的 random 方法生成隨機數。這個方法的實現方式是: os.cpp:
//初始seed,默認是1
volatile unsigned int os::_rand_seed = 1;
static int random_helper(unsigned int rand_seed) {
/* standard, well-known linear congruential random generator with
* next_rand = (16807*seed) mod (2**31-1)
* see
* (1) "Random Number Generators: Good Ones Are Hard to Find",
* S.K. Park and K.W. Miller, Communications of the ACM 31:10 (Oct 1988),
* (2) "Two Fast Implementations of the 'Minimal Standard' Random
* Number Generator", David G. Carta, Comm. ACM 33, 1 (Jan 1990), pp. 87-88.
*/
const unsigned int a = 16807;
const unsigned int m = 2147483647;
const int q = m / a; assert(q == 127773, "weird math");
const int r = m % a; assert(r == 2836, "weird math");
// compute az=2^31p+q
unsigned int lo = a * (rand_seed & 0xFFFF);
unsigned int hi = a * (rand_seed >> 16);
lo += (hi & 0x7FFF) << 16;
// if q overflowed, ignore the overflow and increment q
if (lo > m) {
lo &= m;
++lo;
}
lo += hi >> 15;
// if (p+q) overflowed, ignore the overflow and increment (p+q)
if (lo > m) {
lo &= m;
++lo;
}
return lo;
}
int os::random() {
// Make updating the random seed thread safe.
while (true) {
unsigned int seed = _rand_seed;
unsigned int rand = random_helper(seed);
//CAS更新
if (Atomic::cmpxchg(&_rand_seed, seed, rand) == seed) {
return static_cast<int>(rand);
}
}
}
其中,random_helper 就是隨機數的生成公式的實現,公式是: 這裏,a=16807, c=0, m=2^31-1
由於這些隨機數都是採用的同一個生成器,會 CAS 更新同一個 seed,如果有大量的生成的新對象並且都調用hashcode()
方法的話,可能會有性能問題。重複調用同一個對象的hashcode()
方法不會有問題,因爲之前提到了是有緩存的。
-XX:hashCode=1或者4 基於對象指針 OOPs
OOPs(Ordinary Object Pointers)對象指針是對象頭的一部分。關於對象頭結構,以及對象存儲結構,感興趣的話,可以參考:Java GC詳解 - 1. 理解Java對象結構。可以簡單理解爲對象在內存中的地址的描述。
else if (hashCode == 1) {
// This variation has the property of being stable (idempotent)
// between STW operations. This can be useful in some of the 1-0
// synchronization schemes.
intptr_t addr_bits = cast_from_oop<intptr_t>(obj) >> 3;
value = addr_bits ^ (addr_bits >> 5) ^ GVars.stw_random;
}
else if (hashCode == 4) {
value = cast_from_oop<intptr_t>(obj);
}
cast_from_oop
很簡單,就是獲取oop
的實現基類oopDesc
的指向地址(oopDesc
描述了OOP的基本組成,感興趣可以參考:Java GC詳解 - 1. 理解Java對象結構):
template <class T> inline T cast_from_oop(oop o) {
return (T)(CHECK_UNHANDLED_OOPS_ONLY((oopDesc*))o);
}
當-XX:hashCode=4
,直接用oop
的地址作爲哈希值。-XX:hashCode=1
則是經過變換的,每次發生 Stop The World (STW)stw_random會發生改變,通過這個addr_bits ^ (addr_bits >> 5) ^ GVars.stw_random
變換減少哈希碰撞,讓哈希值更散列化。
想更深入瞭解 Stop the world,可以參考:JVM相關 - SafePoint 與 Stop The World 全解(基於OpenJDK 11版本)
-XX:hashCode=2 敏感測試,恆定爲1
else if (hashCode == 2) {
value = 1; // for sensitivity testing
}
主要用於測試某些集合是否對於哈希值敏感。
-XX:hashCode=3 自增序列
else if (hashCode == 3) {
value = ++GVars.hc_sequence;
}
struct SharedGlobals {
// omitted
DEFINE_PAD_MINUS_SIZE(1, DEFAULT_CACHE_LINE_SIZE, sizeof(volatile int) * 2);
// Hot RW variable -- Sequester to avoid false-sharing
volatile int hc_sequence;
DEFINE_PAD_MINUS_SIZE(2, DEFAULT_CACHE_LINE_SIZE, sizeof(volatile int));
};
static SharedGlobals GVars;
每創建一個新對象,調用哈希值,這個自增數+1,可以看出,散列性極差,很容易哈希碰撞。
-XX:hashCode=5 默認實現
else {
// Marsaglia's xor-shift scheme with thread-specific state
// This is probably the best overall implementation -- we'll
// likely make this the default in future releases.
unsigned t = self->_hashStateX;
t ^= (t << 11);
self->_hashStateX = self->_hashStateY;
self->_hashStateY = self->_hashStateZ;
self->_hashStateZ = self->_hashStateW;
unsigned v = self->_hashStateW;
v = (v ^ (v >> 19)) ^ (t ^ (t >> 8));
self->_hashStateW = v;
value = v;
}
採用的算法是 Marsaglia's xor-shift 隨機數生成法。主要是這篇論文提出的一種快速並且散列性好的哈希算法。
特殊的哈希值導致某些場景的問題
我們經常使用某個對象或者某個字段的哈希值,通過對於某個數組長度取模,獲取到下標,取出數組對應下標的對象,進行進一步處理。這在負載均衡,任務調度,線程分配很常見。那下面這段代碼是否有問題呢?
//獲取userId這個字符串的哈希值的絕對值
int index = Math.abs(userId.hashCode());
//返回哈希值取模之後的下標的對象
return userAvatarList.get(index % userAvatarList.size()).getUrl();
通常大多數情況下,是沒有問題的,但是假設userId
是這幾個哈希值爲Integer.MIN_VALUE
的字符串:
System.out.println("polygenelubricants".hashCode());
System.out.println("GydZG_".hashCode());
System.out.println("DESIGNING WORKHOUSES".hashCode());
輸出:
-2147483648
-2147483648
-2147483648
對於這些值,如果你用Math.abs()
取絕對值的話,我們知道Math.abs(Integer.MIN_VALUE)
還是等於Integer.MIN_VALUE
,這是因爲底層實現:
public static int abs(int a) {
return (a < 0) ? -a : a;
}
-Integer.MIN_VALUE
和Integer.MIN_VALUE
是相等的。Integer.MIN_VALUE
取模還是負數,這樣取下標對應的對象的時候,就會報異常。
所以,需要修改爲:
int index = Math.abs(userId.hashCode() % userAvatarList.size());
return userAvatarList.get(index).getUrl();