我承认,看过亿点点。
你好呀,我是歪歪。
我不知道为什么,好像真的有很多 Java 程序员朋友认为对象的 hashCode 默认和其内存地址相关。
先声明一下:重写了 hashCode 方法的,比如 String 不在讨论范围内。
我第一次听到这个说法的时候,虽然当时没有去研究这个 hashCode,但是我隐隐约约就觉得不对劲。
你想啊,GC 算法,是不是有“标记-复制”、“标记-整理”算法的?
你再寻思一下...
算了,别寻思了,我截个图:
Whenever it is invoked on the same object more than once during an execution of a Java application, the {@code hashCode} method must consistently return the same integer, provided no information used in {@code equals} comparisons on the object is modified.
这是啥意思呢?
就是说:在一个 Java 应用程序的执行过程中,无论何时对同一个对象调用多次 hashCode 方法都必须始终如一地返回相同的整数,前提是对象上用于 equals 比较的信息没有被修改。
如果对象的内存地址变化了,hashCode 也不应该变啊。
所以从这个角度来说,hashCode 也不应该和对象的内存地址相关。
那么,源码之下无秘密,一起去源码里面遨游一圈。
但是,通过上面的截图,可以看到 Object 类的 hashCode 方法是 native 的。
咋整?
找对应的 native 源码不就行了。
去哪儿找?
Oracle JDK 是看不成了,但是我们可以看看 openJDK 啊。
毕竟R大曾经说过:Oracle JDK与OpenJDK里的JVM都是HotSpot VM。从源码层面说,两者基本上是同一个东西。
https://www.zhihu.com/question/19882320
所以我们去翻一下 OpenJDK 的源码就行了。
https://hg.openjdk.java.net/jdk8u/jdk8u
我之前有一份源码,所以直接先找到 Object 类 hashCode 方法。这就是我要入手的地方:
打开 Object.c 文件后发现,这里调用的是 JVM_IHashCode 方法指针:
而 JVM_IHashCode 位于 jvm.cpp 文件中:
https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/1bcfb8cc3c6d/src/share/vm/prims/jvm.cpp
结果发现还是一个套娃的地方,调用了 ObjectSynchronizer::FastHashCode
而该方法属于 synchronizer.cpp 文件:
https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/87ee5ee27509/src/share/vm/runtime/synchronizer.cpp
注意啊,同志们,我框起来的地方:
hash = get_next_hash(Self, obj); // allocate a new hash code
get_next_hash 方法就是真正生成 hashCode 的方法,我把这个方法粘出来,一起分析一波:
首先,一眼瞟过去,代码结构是非常清晰的,一共 6 种情况。
我们按照代码分支判断,一个个的说。
第 0 种情况:看注释说的是采用的一种叫做 Park-Miller RNG 的随机数生成策略。去看了一眼实现方式,直接就是一波劝退,咱也不懂,反正就当是随机数处理了。
第 1 种情况:这种确实是基于对象的内存地址生成的。
第 2 种情况:就直接返回 1,后面跟着一个注释“用于敏感性测试”。咱也不懂,反正就是测试用。
第 3 种情况:自增序列,没啥说的。
第 4 种情况:也是对象内存地址,只是用法和第一种情况不一样而已。
第 5 种情况:有点复杂,反正是没看太明白。但是看注释看明白了,说的是“使用线程的状态结合 xorshift 算法生成”。看这里面具体实现,用了好几个位运算,虽然看不懂,反正牛逼,速度快就完事了。
所以,综上:在 HotSpot 中,一个对象的 hashCode 可以和内存地址有关,也可以和内存地址无关。到底有没有关系,取决于默认用什么算法。
而默认用什么算法,在哪儿定义的呢?
就在 globals.hpp 文件中:
https://github.com/openjdk/jdk/blob/7ba83041b1d65545833655293d0976dfd1ffdea8/hotspot/src/share/vm/runtime/globals.hpp
在 JDK 8 中,哦,不对,严谨点,至少在 openJDK8 中,hashCode 的默认实现方式是前面说的第 5 种情况,和对象的内存地址没有半毛钱关系。
我就纳了闷了,“hashCode 的值和对象内存地址相关”这个说法是哪里来的呢?
电光火石之间,我想不会是历史版本里面的“坑”吧?
赶紧追溯了一下 JDK 7 的默认值是啥:
http://hg.openjdk.java.net/jdk7u/jdk7u/hotspot/file/5b9a416a5632/src/share/vm/runtime/globals.hpp
果然不一样了,采用的是第 0 种情况,随机数的方案。
但是我寻思也和内存地址没有关系啊?
难道在更早的版本里面?
这我就没必要再去验证了,毕竟我相信现在大家在生产上跑着的 JDK 版本最低也得是 7 了吧,再往前的版本号,有兴趣的朋友可以自己去追随一下。
但是还有一个需要强调的是,上面的结论是来源于我们最常用的 HotSpot VM。也许在其他的虚拟机上,有着完全不同的实现,也许就是和内存地址强关联的。
到时候你看到了其他的虚拟机的实现,发现和我这里说的不一样,可别说我胡说啊。
另外,我对上面说的第 2 种情况,很感兴趣:
if (hashCode == 2) {
value = 1 ; // for sensitivity testing
}
我们可以用 jvm 启动参数来替换掉 JDK 8 的默认实现:
-XX:hashCode=2
来看实验。
正常启动的时候是这样的:
加入 jvm 启动参数后再次运行:
全部都变成 1 了,有点意思吧。
好了,本文就这些内容了。
那你看完了,我问你一个问题:
你觉得你知道了这个点,有什么卵用吗?
是的,没有。
那么恭喜你,又在我这里学到了一个没有任何卵用的知识点。
等等,别走,我还有句话要说:
推荐👍 :就这?一个没啥卵用的知识点。
推荐👍 :我不服!这开源项目居然才888个星!?
推荐👍 :曝光一个网站,我周末就耗在上面了。
我是 why,一个主要写代码,经常写文章,偶尔拍视频的程序猿。
欢迎关注我呀。