(给ImportNew加星标,提高Java技能)
编译:ImportNew/唐尤华
srvaroa.github.io/jvm/java/openjdk/biased-locking/2017/01/30/hashCode.html
本文由浅入深,由hashCode()表面入手,深入JVM源代码从对象布局、偏向锁角度分析默认hashCode()对程序性能产生的巨大影响。
非常感谢Gil Tene和Duarte Nunes对本文审查、建议和编辑,并且提出了非常宝贵的意见。文中如有错误都是我的问题。
细节之谜
上周,我在工作中对一个类进行了一次不起眼的修改,把toString()的实现做了改进,这样日志看起来更容易理解。结果让我大吃一惊,修改后测试通过率下降了近5%。我知道单元测试已经覆盖了所有新的代码,究竟哪里出了问题?比较测试报告时,一位同事敏锐地发现修改hashCode()前这些测试都没有问题。当然,这是有道理的:默认toString()会调用hashCode():
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
覆盖toString()后,自定义实现不再调用hashCode()。这里缺少一个测试。
大家都知道toString()的默认实现,但是……
hashCode()的默认实现是怎样的?
默认hashCode()返回值被称作identity hash code。接下来的文章中,我会用这个术语把它与重写后的hashCode()返回的哈希值进行区分。仅供参考:即使重写了hashCode(),还可以调用System.identityHashCode(o)得到对象的identity hash code。
众所周知,identity hash code使用了内存地址的整数表示。这也是J2SE JavaDocs中关于Object.hashCode()的描述:
...通常把对象内部地址
转换为整数,但这种实现不是
Java™语言本身的要求。
这种实现似乎由问题,因为方法的定义要求:
执行Java程序时,在同一对象上多次调用
hashCode方法结果必须
返回相同的整数。
鉴于JVM会重定位对象(例如,在垃圾回收周期可能发生提升或压缩),因此在对象identity hash计算完成后需要能够确保不受重定位影响。
一种可能是首次调用hashCode()时获得对象当前内存位置,与对象一起保存在某个地方,比如对象头。这样,即使对象被移到内存其它地方也会保留原来的哈希值。采用这种方法需要注意一点,可能产生两个对象具有相同的identity hash,但这是规范允许的。
最好办法是通过源代码确认。不幸的是,默认的java.lang.Object::hashCode() 是一个原生函数:
public native int hashCode();
开始行动。
能否找到真正的hashCode()?
注意:hashCode()实现与JVM相关。本文只讨论OpenJDK源代码,下文中出现JVM时都特指OpenJDK。源代码请参考Hotspot tree中的changeset 5820:87ee5ee27509。尽管大部分实现在Oracle JVM上应该也使用,但其实际可能会有区别。
OpenJDK在src/share/vm/prims/jvm.h和src/share/vm/prims/jvm.cpp中定义了hashCode()入口。jvm.cpp中:
508 JVM_ENTRY(jint, JVM_IHashCode(JNIEnv* env, jobject handle))
509 JVMWrapper("JVM_IHashCode");
510 // 在经典虚拟机中实现;如果对象为NULL,则返回0
511 return handle == NULL ? 0 : ObjectSynchronizer::FastHashCode (THREAD, JNIHandles::resolve_non_null(handle)) ;
512 JVM_END
ObjectSynchronizer::FastHashCode()还被identity_hash_value_for调用,后者有几个地方会调用(例如: