如何在Java中将32位hashCode存储在25位标记字中而不会丢失数据?

kmbjn2e3  于 2023-05-12  发布在  Java
关注(0)|答案(1)|浏览(191)

我一直在研究Java对象的内部,对如何管理hashCode值感到困惑。据我所知,Java中的hashCode方法返回一个32位整数。然而,这个hashCode存储在对象的头部,特别是在25位标记字中。
这给我提出了几个问题:
如何在25位标记字中存储32位hashCode而不丢失一些数据位?即使由于这种位长差异而发生数据丢失,为什么当我再次调用hashCode()时,它仍然检索原始的hashCode值,而没有任何明显的数据丢失?
任何关于Java如何做到这一点的见解将不胜感激。

cigdeys3

cigdeys31#

首先,也是最重要的一点:所有这些都是一个实现细节,没有在规范中定义。我专门讨论了最近的OpenJDK构建(我正在测试JDK 17,但这种行为似乎存在了一段时间),但没有任何迹象表明其他JDK甚至OpenJDK的未来版本可以改变这一点。
接下来,区分对象的 * 身份哈希代码 * 和它的 * 哈希代码 * 很重要。

  • identity hash code 是一个由JVM决定的值,它在对象的生命周期中是恒定的,并且不会受到Java代码的影响(即重写hashCode()对此没有影响)。这个值可以通过调用System.identityHashCode(obj)得到。
  • 另一方面,hash code 是Java程序员主要与之交互的:在对象上调用时hashCode()的返回值。虽然这是一个重要的值,但只要有任何东西存储在HashMapHashSet(或类似的结构)中,JVM本身并不特别关心它。即使它这样做了,它也不能将其存储在对象头中,因为hashCode()可以想象每次调用它时都会返回不同的值。

这两个定义以一种重要的方式相互作用:java.lang.ObjecthashCode()方法(以及该方法未被覆盖的任何其他对象)将返回标识哈希代码。因此,如果没有定义其他值,可以说身份哈希代码是哈希代码的默认值。
在看了the relevant code之后,在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位(如上面链接的实现的注解中所述),尽管有大量的空闲空间(在这种情况下,许多位未使用)。

相关问题