侧边栏壁纸
博主头像
why

一个主要敲代码,经常怼文章,偶尔拍视频的成都人。

  • 累计撰写 181 篇文章
  • 累计创建 11 个标签
  • 累计收到 88 条评论

我承认,看过亿点点。

why
why
2021-07-09 / 0 评论 / 1 点赞 / 244 阅读 / 2,696 字
温馨提示:
关注公众号why技术,第一时间接收最新文章。

你好呀,我是歪歪。

我不知道为什么,好像真的有很多 Java 程序员朋友认为对象的 hashCode 默认和其内存地址相关。

先声明一下:上面说的对象是指自定义对象,且没有重写 hashCode。

我第一次听到这个说法的时候,虽然当时没有去研究这个 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 了,有点意思吧。

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

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

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

是的,没有。

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

最后说一句(求关注)

好了,看到了这里了,关注一下我的公众号[why技术]吧,文章写好后第一时间会先发布在公众号里面。

写文章很累的,需要一点正反馈。你的关注,就是强有力的正反馈!

才疏学浅,难免会有纰漏,如果你发现了错误的地方,还请你留言给我指出来,我对其加以修改。

抱拳了,铁子!

0

评论区