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
时,似乎按原样使用对象的内存地址; - 第六个似乎使用了
Marsaglia
的Xorshift
方案。换句话说,根据执行线程的某种状态,它生成下一个伪随机数。
请对这份清单持保留态度。这些只是我阅读代码时的假设。
现在的问题是:
默认情况下使用哪种生成方法?和如何选择使用哪种生成方法?
让我们调查一下。
在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哈希状态值。Y
和Z
用于存储之前生成的两个值。
在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
暂无评论