hashCode,一个实验引发的思考

一个有趣的实验



说明:
更换输出顺序后,输出的结果并没有改变,让人好疑惑。


思考:

  • System.out.println( o )方法在编译后,会在对象o上自动加上".toString( )",即实际打印的是o.toString( )的结果。
  • 再看Object.toString( )的源码:显然,上述打印结果@后面的值,就是hashCode的十六进制表示。
public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
  • 由此知道问题出在hashCode()身上。很多人说,那是对象的内存地址,但是,每次执行这段代码,输出都没变,觉得不太可能。
  • 怎么办?继续往下查,势必要把真凶揪出来。

hashCode,什么鬼?

hashCode是 java.lang.Object.hashCode() 或者 java.lang.System.identityHashCode(obj) 会返回的值。他是一个对象的身份标识。官方称呼为:标识哈希码( identity hash code)。

哪些特点?

  1. 一个对象在其生命期中 identity hash code 必定保持不变;
  2. 如果a == b,那么他们的System.identityHashCode() 必须相等;
    如果他们的System.identityHashCode() 不相等,那他们必定不是同一个对象(逆否命题与原命题真实性总是相同);
  3. 如果System.identityHashCode() 相等的话,并不能保证 a == b(毕竟这只是一个散列值,是允许冲突的)。

有什么作用?

  • 加速对象去重:由特征2可知,只要判断出两个对象的hashCode不一致,就知道这两个对象不是同一个;又因为hashCode()的性能比 " == "性能高得多,所以多数时候,用它来判断重复。

怎么计算出来的?

hashCode( )源码:

public native int hashCode();

很遗憾,他是一个本地方法,具体实现依赖于操作系统。

网上查询得知,JDK源代码由C++、Java、C、汇编 这四种语言组成。JVM主体是C++写的,JNI部分是C,工具类是Java写的,JVM里混有汇编代码。

而且JDK里包含了本地方法的实现源码,我们在src/share/vm/prims/jvm.h和src/share/vm/prims/jvm.cpp中可以找到。

我们重点看以下方法:

// hashCode() generation :  
//  
// Possibilities:  
// * MD5Digest of {obj,stwRandom}  
// * CRC32 of {obj,stwRandom} or any linear-feedback shift register function.  
// * A DES- or AES-style SBox[] mechanism  
// * One of the Phi-based schemes, such as:  
//   2654435761 = 2^32 * Phi (golden ratio)  
//   HashCodeValue = ((uintptr_t(obj) >> 3) * 2654435761) ^ GVars.stwRandom ;  
// * A variation of Marsaglia's shift-xor RNG scheme.  
// * (obj ^ stwRandom) is appealing, but can result  
//   in undesirable regularity in the hashCode values of adjacent objects  
//   (objects allocated back-to-back, in particular).  This could potentially  
//   result in hashtable collisions and reduced hashtable efficiency.  
//   There are simple ways to "diffuse" the middle address bits over the  
//   generated hashCode values:  
//  

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 变量值的六种方法。怎么生成最终值取决于hashCode这个变量值。

  • 0 - 使用Park-Miller伪随机数生成器(跟地址无关)
  • 1 - 使用地址与一个随机数做异或(地址是输入因素的一部分)
  • 2 - 总是返回常量1作为所有对象的identity hash code(跟地址无关)
  • 3 - 使用全局的递增数列(跟地址无关)
  • 4 - 使用对象地址的“当前”地址来作为它的identity hash code(就是当前地址)
  • 5 - 使用线程局部状态来实现Marsaglia's xor-shift随机数生成(跟地址无关)


VM到底用的是哪种方法?

JDK 8 和 JDK 9 默认值:

product(intx, hashCode, 5,"(Unstable) select hashCode generation algorithm") ; 

JDK 8 以前默认值:

product(intx, hashCode, 0,"(Unstable) select hashCode generation algorithm") ; 

不同的JDK,生成方式不一样。

注意:
虽然方式不一样,但有个共同点:java生成的hashCode和对象内存地址没什么关系。
是不是有点出乎意料呢?

修改生成方法?

HotSpot提供了一个VM参数来让用户选择identity hash code的生成方式:

-XX:hashCode


什么时候计算出来的?

在VM里,Java对象会在首次真正使用到它的identity hash code(例如通过Object.hashCode() / System.identityHashCode())时调用VM里的函数来计算出值,然后会保存在对象里,后面对同一对象查询其identity hash code时总是会返回最初记录的值。
因此,不是对象创建时。

这组实现代码在HotSpot VM里自从JDK6的早期开发版开始就没变过,只是hashCode选项的默认值变了而已。


实验结果释疑

上面的程序在执行到这个 hashCode() 调用时,VM看到对象之前还没计算 identity hash code,才去计算并记录它。

这样的话,先 println(arr1) 就会使得 arr0 所引用的数组对象先被计算 identity hash code,在VM上就是从伪随机数列中取出某一项,然后再 println(arr2) 就会计算并记录 arr2 所引用的数组对象的 hash code,也就是取出那个伪随机数列的下一项。反之亦然。

所以无论先 println(arr1) 还是先 println(arr2) ,看到的都是 VM用来实现 identity hash code 的伪随机数列的某个位置的相邻两项,自然怎么交换都会看到一样的结果。


验证结论

int[] arr0 = new int[3];
int[] arr1 = new int[3];
arr0.hashCode(); // 触发arr0计算identity hash code
arr1.hashCode(); // 触发arr1计算identity hash code

// 试着交换下面两行
System.out.println(arr0);
System.out.println(arr1);

执行后,可以看到输出结果已经交换。

成功找到真凶,✌️ 。

编辑于 2017-08-03

文章被以下专栏收录