2周前 (11-04)  Java系列 |   抢沙发  6 
文章评分 0 次,平均分 0.0

OpenJDK上正在进行一个名为Lilliput的项目(https://openjdk.org/projects/lilliput/)。它是关于减小Hotspot JVM中对象头的大小。

这让我很好奇:当程序员没有提供对象的哈希码值时,JVM是如何计算对象的哈希代码值的?

换句话说,如果我们不重写类中的Object::hashCode方法,那么当我们在类的实例中调用hashCode时,会返回什么值?

让我们创建一个名为HashCode的类。它将是一个直接的Object子类。

我们只需将类的一个实例打印到控制台:

package blog;

public class HashCode {
  public static void main(String[] args) {
    HashCode o = new HashCode();

    System.out.println(o);
  }
}

输出:

blog.HashCode@543c6f6d

您可能以前见过这种“类名”@“十六进制数”输出。

输出来自Object::toString实现。

以下是Object::toString实现:

public String toString() {
  return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

“@”字符后的值是对象的十六进制哈希码。

此实现是Javadocs方法中的规范所要求的:

Object类的toString方法返回一个字符串,该字符串由对象作为实例的类的名称、at符号字符“@”和对象哈希码的无符号十六进制表示组成。

我们也没有重写Object::hashCode方法。

因此,十六进制值是由Object类的hashCode方法提供的。

以下是Object::hashCode实现:

@IntrinsicCandidate
public native int hashCode();

它是土生土长的。现在怎么办?

让我们看看System::identityHashCode方法。

以下是System::dentityHashCode文档的摘录:

返回给定对象的哈希码,与默认方法hashCode()返回的哈希码相同,无论给定对象的类是否覆盖hashCode)。

那么,我们可以假设Object::hashCode在内部使用它吗?

也许吧。也许不是。

但这些值保证是相同的。

因此,Object::hashCode实现可以认为如下:

public int hashCode() {
  return System.identityHashCode(this);
}

下面是System::identityHashCode方法实现:

@IntrinsicCandidate
public static native int identityHashCode(Object x);

它也是土生土长的。

我们必须在JVM源代码中查找实现

在JVM源代码中搜索哈希代码生成

我们想知道如何在JVM中计算哈希码。

本机java.lang.Object

我们知道System::identityHashCode计算我们要查找的值。但是,为了确保万无一失,让我们看看原生Object::hashCode实现。

我能找到的最好的是src/java.base/share/national/libjava/Object.c文件。但它只提供了原生的getClass方法实现:

JNIEXPORT jclass JNICALL
Java_java_lang_Object_getClass(JNIEnv *env, jobject this)
{
    if (this == NULL) {
        JNU_ThrowNullPointerException(env, NULL);
        return 0;
    } else {
        return (*env)->GetObjectClass(env, this);
    }
}

因此,我没有找到本机Object::hashCode实现。

让我们继续讨论System::identityHashCode方法。

本机java.lang.System

src/java.base/share/national/libjava/System.c文件中可以找到java.lang.System类的本机部分。

以下是identityHashCode方法的代码:

JNIEXPORT jint JNICALL
Java_java_lang_System_identityHashCode(JNIEnv *env, jobject this, jobject x)
{
    return JVM_IHashCode(env, x);
}

它委托给JVM_IHashCode符号。

我不懂C++,所以我不知道这个符号是指函数、方法还是宏。

无论如何,让我们继续搜索。

我们在src/hotstop/share/prims/JVM.cpp文件中找到JVM_IHashCode符号:

JVM_ENTRY(jint, JVM_IHashCode(JNIEnv* env, jobject handle))
  // as implemented in the classic virtual machine; return 0 if object is null
  return handle == nullptr ? 0 :
         checked_cast<jint>(ObjectSynchronizer::FastHashCode (THREAD, JNIHandles::resolve_non_null(handle)));
JVM_END

再说一次,我不懂C++。我也不知道JVM的源代码。

但我假设它正在委托给ObjectSynchronizer::FastHashCode方法。

src/hotstop/share/runtime/synchronizer.cpp文件中,我们找到了FastHashCode方法。

这是一个相当长的方法。所以这里有一个相关的部分:

hash = mark.hash();
if (hash != 0) {                     // if it has a hash, just return it
  return hash;
}
hash = get_next_hash(current, obj);  // get a new hash

我假设mark变量是指Java对象头的标记字。

因此,如果哈希值尚未计算,它将调用get_ext_hash函数。

我相信这就是我们正在寻找的代码。

在同一个文件中,我们找到了get_ext_hash函数。

以下是删除了大部分注释的代码:

static inline intptr_t get_next_hash(Thread* current, oop obj) {
  intptr_t value = 0;
  if (hashCode == 0) {
    value = os::random();
  } else if (hashCode == 1) {
    intptr_t addr_bits = cast_from_oop<intptr_t>(obj) >> 3;
    value = addr_bits ^ (addr_bits >> 5) ^ GVars.stw_random;
  } else if (hashCode == 2) {
    value = 1;            // for sensitivity testing
  } else if (hashCode == 3) {
    value = ++GVars.hc_sequence;
  } else if (hashCode == 4) {
    value = cast_from_oop<intptr_t>(obj);
  } else {
    // Marsaglia's xor-shift scheme with thread-specific state
    ...
    unsigned t = current->_hashStateX;
    t ^= (t << 11);
    current->_hashStateX = current->_hashStateY;
    current->_hashStateY = current->_hashStateZ;
    current->_hashStateZ = current->_hashStateW;
    unsigned v = current->_hashStateW;
    v = (v ^ (v >> 19)) ^ (t ^ (t >> 8));
    current->_hashStateW = v;
    value = v;
  }

  ...
}

哈希码的生成依赖于名为hashCode的外部标志。

目前有六种不同的生成方法:

  • 第一个,当hashCode==0时,似乎得到了底层操作系统生成的下一个伪随机值;
  • 第二个,当hashCode==1时,似乎使用对象的内存地址。它在生成最终值之前将比特混合在一起;
  • 第三个,当hashCode==2时,简单地将值设置为1;
  • 第四个,当hashCode==3时,似乎会递增,然后得到一个全局变量的值;
  • 第五个,当hashCode==4时,似乎按原样使用对象的内存地址;
  • 第六个似乎使用了MarsagliaXorshift方案。换句话说,根据执行线程的某种状态,它生成下一个伪随机数。

请对这份清单持保留态度。这些只是我阅读代码时的假设。

现在的问题是:

默认情况下使用哪种生成方法?和如何选择使用哪种生成方法?

让我们调查一下。

src/hotstop/share/runtime/globals.hpp文件中,我们找到了hashCode标志:

product(intx, hashCode, 5, EXPERIMENTAL,
             "(Unstable) select hashCode generation algorithm")

所以看起来:

其默认值为5。

我们可以通过运行以下命令进行确认:

java -XX:+UnlockExperimentalVMOptions \
     -XX:+PrintFlagsFinal 2>/dev/null | grep hashCode

输出:

intx hashCode = 5 {experimental} {default}

因此,我们可以通过-XX:hashCode=value JVM选项进行设置。

本机Thread类

因此,默认情况下,JVM将使用Marsaglia Xorshift算法来实现identityHashCode方法:

// Marsaglia's xor-shift scheme with thread-specific state
...
unsigned t = current->_hashStateX;
t ^= (t << 11);
current->_hashStateX = current->_hashStateY;
current->_hashStateY = current->_hashStateZ;
current->_hashStateZ = current->_hashStateW;
unsigned v = current->_hashStateW;
v = (v ^ (v >> 19)) ^ (t ^ (t >> 8));
current->_hashStateW = v;
value = v;

当前变量指的是当前线程。该代码直接使用X和W哈希状态值。YZ用于存储之前生成的两个值。

src/hotstop/share/runtime/thread.cpp文件中,我们可以找到它们的初始值:

// thread-specific hashCode stream generator state - Marsaglia shift-xor form
_hashStateX = os::random();
_hashStateY = 842502087;
_hashStateZ = 0x8767;    // (int)(3579807591LL & 0xffff) ;
_hashStateW = 273326509;

需要注意的重要一点是,X值是用操作系统中的下一个伪随机数初始化的。

测试我们的发现

让我们回到我们的运行示例。

对于六种不同的生成方法,我们将运行我们的示例三次。

因此,在命令行中,我们定义了以下函数:

hashCode() { for i in {1..3}; do java -XX:+UnlockExperimentalVMOptions -XX:hashCode=${1} HashCode.java; done }

这就是我们将用来运行示例的内容。

使用-XX:hashCode=0运行

它应该使用以下哈希码生成:

value = os::random();

当我们在命令行中运行hashCode 0时,我们得到:

blog.HashCode@387162d9
blog.HashCode@387162d9
blog.HashCode@387162d9

有趣。它为不同的JVM运行生成相同的哈希代码值。

-XX:hashCode=0选项无效,或者os::random()使用固定的随机种子。

我认为是后者。

使用-XX:hashCode=1运行

它应该使用以下哈希码生成:

intptr_t addr_bits = cast_from_oop<intptr_t>(obj) >> 3;
value = addr_bits ^ (addr_bits >> 5) ^ GVars.stw_random;

由于它似乎使用了对象的内存地址,我们应该期望在不同的运行中有不同的值。

当我们在命令行中运行hashCode 1时,我们得到:

blog.HashCode@633c0297
blog.HashCode@633c02eb
blog.HashCode@633df072

好的。我们每次运行都有不同的值。

使用-XX:hashCode=2运行

它应该使用以下哈希码生成:

value = 1

当我们在命令行中运行hashCode 2时,我们得到:

blog.HashCode@1
blog.HashCode@1
blog.HashCode@1

所以我们得到了恒定的哈希码。

使用-XX:hashCode=3运行

它应该使用以下哈希码生成:

value = ++GVars.hc_sequence;

当我们在命令行中运行hashCode 3时,我们得到:

blog.HashCode@5d1
blog.HashCode@5d1
blog.HashCode@5d1

哈希码值都是相同的,因为它是同一个对象实例。

我们需要使用不同的程序:

public class HashCode3 {
  public static void main(String[] args) {
    System.out.println(new HashCode3());
    System.out.println(new HashCode3());
    System.out.println(new HashCode3());
  }
}

它创建三个不同的实例,并打印每个实例。

让我们运行一次:

java -XX:+UnlockExperimentalVMOptions -XX:hashCode=3 HashCode.java

输出:

blog.HashCode3@5da
blog.HashCode3@5db
blog.HashCode3@5dc

哈希码值按顺序排列。

使用-XX:hashCode=4运行它应该使用以下哈希码生成:

value = cast_from_oop<intptr_t>(obj);

它似乎也使用了对象的内存地址。我们应该期望每次运行都有不同的值。

当我们在命令行中运行hashCode 4时,我们得到:

blog.HashCode@20e8c4d0
blog.HashCode@20e72e28
blog.HashCode@20e76398

所以我们每次运行都有不同的值。

使用-XX:hashCode=5运行

它应该使用Marsaglia Xorshit哈希码生成:

unsigned t = current->_hashStateX;
t ^= (t << 11);
current->_hashStateX = current->_hashStateY;
current->_hashStateY = current->_hashStateZ;
current->_hashStateZ = current->_hashStateW;
unsigned v = current->_hashStateW;
v = (v ^ (v >> 19)) ^ (t ^ (t >> 8));
current->_hashStateW = v;
value = v;

当我们在命令行中运行hashCode 5时,我们得到:

blog.HashCode@543c6f6d
blog.HashCode@543c6f6d
blog.HashCode@543c6f6d

与第一次运行类似,每次不同的运行都会得到相同的值。

正如“原生Thread类”一节所讨论的,这一代依赖于os::random()

而且,如前所述,os::random()生成器的种子似乎是一个固定数字:

volatile unsigned int os::_rand_seed      = 1234567;

最后一点,由于这是默认选项,当我们运行没有选项的程序时,我们应该得到相同的值:

$ java HashCode.java
blog.HashCode@543c6f6d

我们得到了相同的哈希码值。

结论

因此,如果我们不重写类中的Object::hashCode方法,那么当我们在类的实例中调用hashCode时,会返回什么值?

System::identityHashCode静态方法负责返回值。

如果对象还没有计算出身份哈希码,则HotSpot JVM中的get_ext_hash函数会生成一个新的身份哈希码。

在撰写本文时,get_ext_hash函数提供了六种不同的方法来生成哈希码。

我们可以使用-XX:hashCode=value JVM选项来选择生成方法。

默认生成方法是使用Marsaglia Xorshift方法的thread-local伪随机生成器。

您可以在这个GitHub中(https://github.com/objectos/blog-examples/tree/main/2024/02/25)找到示例的源代码。

原文链接:https://www.objectos.com.br/blog/the-java-system-identity-hash-code-method.html

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/2996.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册