TOC
图片来源:维基百科
#从类加载的问题说起
同事在开发中遇到一个关于hashCode的诡异问题,为了简化问题突出本质,现将具体的业务代码进行了抽象,请看以下的代码:
C.jar:
SomeClass.class
A.jar:
Class.forName("SomeClass").hashCode();
B.jar:
someClassObject.getClass().hashCode();
有一个类姑且称之为SomeClass位于C.jar中,它有一个对象someClassObject, A.jar里使用了Class.forName(“SomeClass”).hashCode()获取SomeClass的hashCode,B.jar里使用了someClassObject.getClass().hashCode()来获取hashCode。A.jar,B.jar均依赖C.jar, 三个Jar包都运行于同一个JVM中,以上是问题的上下文。问题出现在代码运行中发现两个语句得到的hashCode尽然不相等!
遇到这种问题第一直觉就是两个jar包的ClassLoader不同,后来找到问题的根源的确是ClassLoader引起的。下面是该环境的ClassLoader层次结构:
Bootstrap ClassLoader
|
Extension ClassLoader
|
Application ClassLoader
|
Customize ClassLoader
问题出现在: B.jar被Customize ClassLoader先加载,A.jar被Application ClassLoader后加载,SomeClass类同时存在于Application ClassLoader和Customize ClassLoader中,从而造成了hashCode不等。
因为其中的原因比较复杂, 本文先不去深究为什么Customize ClassLoader没有委托Application ClassLoader去加载SomeClass。由上面这个问题引申出来的另一个问题:hashCode()的实现细节才是本文的讨论重点。
#深入理解hashCode() 在Java语言里,每个对象都有hashCode()这个方法,该方法是从Object继承而来的。hashCode()方法的存在意义在于对象之间相等的比较,譬如将一个对象put到HashMap当中,HashMap就会调用对象的hashCode()来进一步计算该对象的哈希值。
对于hashCode()这个方法有以下的约定:
在同一个Java进程中,相等的对象hashCode()返回值必须相等
但以下观点是错误的:
1. 不相等的对象hashCode()一定不等
2. hashCode()返回值相等的对象就一定相等
这里先提一个问题,下面两条语句得到的结果一定不等吗?
Class.forName("AClass").hashCode();
Class.forName("BClass").hashCode();
由前面的观点可知不一定,究其原因,就得对hashCode的实现细节进行深入点的研究。不同JDK的实现可能不同,我们来看看OpenJDK7的实现代码synchronizer.cpp
static inline intptr_t get_next_hash(Thread * Self, oop obj) {
intptr_t value = 0 ;
if (hashCode == 0) {
// This form uses an unguarded global Park-Miller RNG,
// so it's possible for two threads to race and generate the same RNG.
// On MP system we'll have lots of RW access to a global, so the
// mechanism induces lots of coherency traffic.
value = os::random() ;
} 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 addrBits = intptr_t(obj) >> 3 ;
value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
} else
if (hashCode == 2) {
value = 1 ; // for sensitivity testing
} else
if (hashCode == 3) {
value = ++GVars.hcSequence ;
} else
if (hashCode == 4) {
value = intptr_t(obj) ;
} 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 ;
}
value &= markOopDesc::hash_mask;
if (value == 0) value = 0xBAD ;
assert (value != markOopDesc::no_hash, "invariant") ;
TEVENT (hashCode: GENERATE) ;
return value;
}
其中hashCode==0的注释中清清楚楚的写着:
so it's possible for two threads to race and generate the same RNG.
对于多线程的环境下,如果采用hashCode=0的哈希生成算法,两个不同的类是完全有可能生成相同的哈希值。
我们可以通过配置Java启动参数-XX:hashCode=n (from 0 to 5)来控制hashCode()生成算法,0到5所对应的算法如下:
0 – Park-Miller RNG (default) Park-Miller随机数生成器
1 – function of address and some global state
2 – const 1
3 – sequenatial counter
4 – address of an object
5 – thread specific xor-shift
每个选项采用不同的算法来生成对象的哈希值, 其中Park-Miller随机数生成算法可以参考Lehmer random number generator。而第四种算法则直接使用object的内存地址作为哈希值。对于其他几种算法,本文就不再作进一步的探究。