查看原文
其他

我承认,看过亿点点。

why技术 why技术 2022-09-10

你好呀,我是歪歪。

我不知道为什么,好像真的有很多 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 了,有点意思吧。

好了,本文就这些内容了。

那你看完了,我问你一个问题:

你觉得你知道了这个点,有什么卵用吗?

是的,没有。

那么恭喜你,又在我这里学到了一个没有任何卵用的知识点。

等等,别走,我还有句话要说:



推荐👍 :就这样,我走完了程序员的前五年...

推荐👍 :面试官:Java如何绑定线程到指定CPU上执行?

推荐👍 :我去,这是出BUG了呀!

推荐👍 :就这?一个没啥卵用的知识点。

推荐👍 :我不服!这开源项目居然才888个星!?

推荐👍 :曝光一个网站,我周末就耗在上面了。

我是 why,一个主要写代码,经常写文章,偶尔拍视频的程序猿。

欢迎关注我呀。

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存