我一直在研究 Java 对象的内部结构,对 hashCode 值的管理方式感到困惑。据我了解,Java 中的 hashCode 方法返回一个 32 位整数。但是这个hashCode是保存在对象的header中的,具体是25位的mark word中。
这给我提出了几个问题:
如何将一个 32 位的 hashCode 存储在一个 25 位的标记字中而不丢失一些数据位? 即使因为这个位长差异导致数据丢失,为什么我再次调用
hashCode()
时,它仍然检索到原始的hashCode值而没有任何明显的数据丢失?
任何关于 Java 如何做到这一点的见解都将不胜感激。
首先也是最重要的:所有这些都是非常重要的实现细节,并且没有由规范定义。我专门讨论了最近的 OpenJDK 构建(我正在测试 JDK 17,但这种行为似乎存在了一段时间),但没有说其他 JDK 甚至未来版本的 OpenJDK 可以改变这一切。
接下来区分对象身份哈希码和它的哈希码很重要。
identity hash code 是由 JVM 决定的一个值,它在对象的生命周期内保持不变,并且不受 Java 代码的影响(即覆盖
hashCode()
对此没有影响)。这个值可以通过调用System.identityHashCode(obj)
获得。
另一方面,hash code 是 Java 程序员最常与之交互的东西:调用对象时
hashCode()
的返回值。当任何东西存储在 HashMap
或 HashSet
(或类似结构)中时,这是一个重要的值,但 JVM 本身并不特别关心它。即使这样做了,它也无法将其存储在对象标头中,因为 hashCode()
可以想象每次调用时都会返回不同的值。
这两个定义以一种重要的方式相互作用:hashCode()
java.lang.Object
方法(以及未覆盖该方法的任何其他对象)将返回身份哈希码。所以可以说身份哈希码是哈希码的默认值,如果没有其他定义的话。
在查看相关代码之后,确实似乎在 32 位平台上最多有 25 位空间来存储身份哈希码。
但是
hashCode
被定义为32位宽,那怎么可能呢?
简单:这些平台上的身份哈希码根本不会使用超过25位,因此所有未存储的位都已知/假定为零。
虽然我没有找到决定的具体位置(我也没有仔细看),但是可以通过这样的代码轻松验证这一点:
public class MyClass {
public static void main(String args[]) {
int minLeadingZeroes = 32;
for (int i = 0; i < 1_000_000; i++) {
int hash = System.identityHashCode(new Object());
minLeadingZeroes = Math.min(minLeadingZeroes, Integer.numberOfLeadingZeros(hash));
}
System.out.println("Smallest number of leading zeroes in identity hash codes of 1000000 objects = " + minLeadingZeroes);
}
}
使用 64 位 JVM 运行时会打印
Smallest number of leading zeroes in identity hash codes of 1000000 objects = 1
而在 32 位 JVM 上打印
Smallest number of leading zeroes in identity hash codes of 1000000 objects = 7
当然,这不是绝对证据,但在测试一百万个对象时,这些值极不可能是巧合。
还要注意,即使在 64 位 OpenJDK 构建中,身份哈希码也最多使用 31 位(如上面链接的实现的评论中所述),尽管有足够的空闲空间(在这种情况下许多位未使用)。