Java 的 Object.hashCode() 的返回值到底是不是对象内存地址?
刚学 Java 的时候我也有过这种怀疑,但一直没有验证;最近在 OSCHINA 上看到有人在回答问题时也这么说,于是萌生了一探究竟的想法—— java.lang.Object.hashCode()
的返回值到底是不是对象内存地址?
hashCode 契约
说到这个问题,大家的第一反应一定和我一样——去查 Object.hashCode
的源码,但翻开源码,看到的却是这样的(Oracle JDK 8):
/**
* Returns a hash code value for the object. This method is
* supported for the benefit of hash tables such as those provided by
* {@link java.util.HashMap}.
* <p>
* The general contract of {@code hashCode} is:
* <ul>
* <li>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.
* This integer need not remain consistent from one execution of an
* application to another execution of the same application.
* <li>If two objects are equal according to the {@code equals(Object)}
* method, then calling the {@code hashCode} method on each of
* the two objects must produce the same integer result.
* <li>It is <em>not</em> required that if two objects are unequal
* according to the {@link java.lang.Object#equals(java.lang.Object)}
* method, then calling the {@code hashCode} method on each of the
* two objects must produce distinct integer results. However, the
* programmer should be aware that producing distinct integer results
* for unequal objects may improve the performance of hash tables.
* </ul>
* <p>
* As much as is reasonably practical, the hashCode method defined by
* class {@code Object} does return distinct integers for distinct
* objects. (This is typically implemented by converting the internal
* address of the object into an integer, but this implementation
* technique is not required by the
* Java™ programming language.)
*
* @return a hash code value for this object.
* @see java.lang.Object#equals(java.lang.Object)
* @see java.lang.System#identityHashCode
*/
public native int hashCode();
Object.hashCode
是一个 native 方法,看不到源码(Java 代码,Oracle 的 JDK 是看不到的,OpenJDK 或其他开源 JRE 是可以找到对应的 C/C++代码)。
上面这段注释指出了 Object.hashCode() 在 JRE(Java Runtime Library)中应该遵循的一些契约(contract):
(PS:所谓契约当然是大家一致达成的,各个 JVM 厂商都会遵循)
- 一致性(consistent),在程序的一次执行过程中,对同一个对象必须一致地返回同一个整数。
- 如果两个对象通过 equals(Object) 比较,结果相等,那么对这两个对象分别调用 hashCode 方法应该产生相同的整数结果。(PS:这里 equals 和 hashCode 说的都是 Object 类的)
- 如果两个对象通过 java.lang.Object.equals(java.lang.Ojbect) 比较,结果不相等,不必保证对这两个对象分别调用 hashCode 也返回两个不相同的整数。
实际上 java.lang
包里面的类,都是 JRE 必须的,属于运行时库(Runtime Library),这也是为什么很多 JRE 下该类的 class 文件被打包到rt.jar中的原因(应该是 Runtime 的简写)。
而这些运行时库一般都是跟 JDK/JRE 一起发布的;所以,对于不同的 JRE 环境,问题的答案未必相同。
考虑到具体 JVM 厂商实现的 Object.hashCode
相对较复杂,下面先通过另一思路对开头提出的问题进行探究。
最后我们再找一些开源 JRE 的 Object.hashCode
的具体实现作简要分析。
Java 中如何获得对象内存地址?
看不到 Object.hashCode
的源码,反过来,我们可以得到对象的内存地址和 Object.hashCode
比较,也能得出结论。
要验证这个问题自然需要一种得到对象内存地址的方法,但 Java 本身并没有提供类似的方法;这也是我在初学 Java 时没有验证这个问题的原因。
“内存地址”在 Java 里得不到,但在 C/C++中却很容易得到。于是,我们想到——通过 JNI 让 Java 代码调用一段 C/C++代码来得到对象内存地址。
这里可能需要考虑的还有一点——用 Java 什么类型能放得下 C/C++的指针?
在 64 位机器上,C/C++的指针是 8 字节;32 位是 4 字节。
嗯(⊙_⊙)~ 不管怎样 Java 的 long 都是 8 字节,足矣~
Think in Java——接口和测试
假设我们已经有了一个 NativeUtils.java
:
class NativeUtils {
public native static long getNativePointer(Object o);
}
并且已经实现了 getNativePointer
方法。
那么验证开头提出的问题就变得异常简单了:
class TestNativeUtils {
public static void main(String args[]) {
Object o = new Object();
long nptr = NativeUtils.getNativePointer(o);
long hash = o.hashCode();
System.out.println(String.format("hash: %x, nptr: %x", hash, nptr));
}
}
Think in C++——实现 native 方法
好了说干就干,现在就差那个 native 的 getNativePointer
了。
用 javah
生成对应的 .h
文件:$ javah NativeUtils
该命令执行后生成了 NativeUtils.h
:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class NativeUtils */
#ifndef _Included_NativeUtils
#define _Included_NativeUtils
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: NativeUtils
* Method: getNativePointer
* Signature: (Ljava/lang/Object;)J
*/
JNIEXPORT jlong JNICALL Java_NativeUtils_getNativePointer
(JNIEnv *, jclass, jobject);
#ifdef __cplusplus
}
#endif
#endif
接着实现这个 Java_NativeUtils_getNativePointer
,文件命名为 NativeUtils.cc
:
#include "NativeUtils.h"
JNIEXPORT jlong JNICALL
Java_NativeUtils_getNativePointer(JNIEnv *env, jclass clazz, jobject o)
{
return reinterpret_cast<jlong>(o);
}
编译为动态库:
$ g++ -shared -o libnative-utils.so NativeUtils.cc
可能因为找不到 jni.h
,报错:
NativeUtils.h:2:17: fatal error: jni.h: No such file or directory
在 JDK 安装目录下查找 jni.h
:
$ find . -name jni.h ./include/jni.h
知道 jni.h
路径后,用-I 选项加到编译命令上再次编译:
$ g++ -shared -I/usr/lib/jvm/java-7-openjdk-amd64/include/ -o libnative-utils.so NativeUtils.cc
OK, 编译成功,生成了 libnative-utils.so
文件。
Run in shell——在 shell 环境运行
下面就让 TestNativeUtils
在 shell 环境执行起来。
首先,编译 NativeUtils
和 TestNativeUtils
:
$ javac NativeUtils.java
$ javac TestNativeUtils.java
分别生成了 NativeUtils.class
和 TestNativeUtils.class
好了,就差临门一脚了——执行 class 文件:
Exception in thread "main" java.lang.UnsatisfiedLinkError: NativeUtils.getNativePointer(Ljava/lang/Object;)J
at NativeUtils.getNativePointer(Native Method)
at TestNativeUtils.main(TestNativeUtils.java:5)
加载动态库到我们的程序中
到目前为止,我们的 Java 代码并没有实现 NativeUtils.getNativePointer
;所以,会有上面的错误。
必须在调用 NativeUtils.getNativePointer
前,将我们编译好的动态库加载上。可以用 static 代码块,以保证在调用前完成加载;修改后的 NativeUtils
:
class NativeUtils {
static {
System.loadLibrary("native-utils");
}
public native static long getNativePointer(Object o);
}
让 JVM 能找到动态库
再次编译、执行:
$ javac NativeUtils.java
$ java TestNativeUtils
又有错误,但已经和刚才不一样了:
Exception in thread "main" java.lang.UnsatisfiedLinkError: no native-utils in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1886)
at java.lang.Runtime.loadLibrary0(Runtime.java:849)
at java.lang.System.loadLibrary(System.java:1088)
at NativeUtils.<clinit>(NativeUtils.java:3)
at TestNativeUtils.main(TestNativeUtils.java:5)
这次错误是:没有在 java.library.path
中找到 native-utils
,可以在 javac 命令-D 参数上将当期目录加到 java.library.path
上,让其能够找到 native-utils
:
$ java -Djava.library.path=. TestNativeUtils
hash: 4f5f1ace, nptr: 7f223a5fb958
几个 JRE 的具体实现
说道开源的 Java 运行环境,首先能想到的自然是 OpenJDK 和 Android,下面分别简要分析。
hashCode on Android
Android 的 Object.hashCode
和 Oracle JDK 的略有不同:
private transient int shadow$_monitor_;
public int hashCode() {
int lockWord = shadow$_monitor_;
final int lockWordMask = 0xC0000000; // Top 2 bits.
final int lockWordStateHash = 0x80000000; // Top 2 bits are value 2 (kStateHash).
if ((lockWord & lockWordMask) == lockWordStateHash) {
return lockWord & ~lockWordMask; //
}
return System.identityHashCode(this);
}
(PS:估计 shadow$_monitor_
应该是 hash 值的一个 cache,第一次需要计算一下,以后都不用计算了)
第一次执行时, shadow$_monitor_
的值为 0,将会调用 System.identityHashCode
:
public static native int identityHashCode(Object anObject);
而它是一个 native 的方法,对应代码 java_lang_System.cc
:
static jint System_identityHashCode(JNIEnv* env, jclass, jobject javaObject) {
if (UNLIKELY(javaObject == nullptr)) {
return 0;
}
ScopedFastNativeObjectAccess soa(env);
mirror::Object* o = soa.Decode<mirror::Object*>(javaObject);
return static_cast<jint>(o->IdentityHashCode());
}
很显然,这里的关键在于 mirror::Object::IdentityHashCode
:
int32_t Object::IdentityHashCode() const {
mirror::Object* current_this = const_cast<mirror::Object*>(this);
while (true) {
LockWord lw = current_this->GetLockWord(false);
switch (lw.GetState()) {
case LockWord::kUnlocked: { // kUnlocked 是 LockWord 的默认 State 值
// Try to compare and swap in a new hash, if we succeed we will return the hash on the next
// loop iteration.
LockWord hash_word(LockWord::FromHashCode(GenerateIdentityHashCode()));
DCHECK_EQ(hash_word.GetState(), LockWord::kHashCode);
if (const_cast<Object*>(this)->CasLockWordWeakRelaxed(lw, hash_word)) {
return hash_word.GetHashCode();
}
break;
}
case LockWord::kThinLocked: {
// Inflate the thin lock to a monitor and stick the hash code inside of the monitor. May
// fail spuriously.
Thread* self = Thread::Current();
StackHandleScope<1> hs(self);
Handle<mirror::Object> h_this(hs.NewHandle(current_this));
Monitor::InflateThinLocked(self, h_this, lw, GenerateIdentityHashCode());
// A GC may have occurred when we switched to kBlocked.
current_this = h_this.Get();
break;
}
case LockWord::kFatLocked: {
// Already inflated, return the has stored in the monitor.
Monitor* monitor = lw.FatLockMonitor();
DCHECK(monitor != nullptr);
return monitor->GetHashCode();
}
case LockWord::kHashCode: { // 以后调用
return lw.GetHashCode();
}
default: {
LOG(FATAL) << "Invalid state during hashcode " << lw.GetState();
break;
}
}
}
LOG(FATAL) << "Unreachable";
return 0;
}
这段代码可以看出——ART 的 Object.hashCode 确实是有 cache 的,对于同一个 Ojbect,第一次调用 Object.hashCode 将会执行实际的计算并记入 cache,以后直接从 cache 中取出。真正计算 hashcode 的是 GenerateIdentityHashCode:
int32_t Object::GenerateIdentityHashCode() {
static AtomicInteger seed(987654321 + std::time(nullptr));
int32_t expected_value, new_value;
do {
expected_value = static_cast<uint32_t>(seed.LoadRelaxed());
new_value = expected_value * 1103515245 + 12345;
} while ((expected_value & LockWord::kHashMask) == 0 ||
!seed.CompareExchangeWeakRelaxed(expected_value, new_value));
return expected_value & LockWord::kHashMask;
}
从 GenerateIdentityHashCode
可以看出,ART 的 Object.hashCode
的返回值和对象的地址并没有直接的关系。
hashCode on OpenJDK
OpenJDK 项目首页: openjdk.java.net
TODO: OpenJDK 上的 hashCode 具体实现和简要分析 Object.c
static JNINativeMethod methods[] = {
{"hashCode", "()I", (void *)&JVM_IHashCode},
{"wait", "(J)V", (void *)&JVM_MonitorWait},
{"notify", "()V", (void *)&JVM_MonitorNotify},
{"notifyAll", "()V", (void *)&JVM_MonitorNotifyAll},
{"clone", "()Ljava/lang/Object;", (void *)&JVM_Clone},
};
JNIEXPORT void JNICALL
Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls)
{
(*env)->RegisterNatives(env, cls,
methods, sizeof(methods)/sizeof(methods[0]));
}
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
对应的 C 函数为 JVM_IHashCode
,下面需要找到 JVM_IHashCode
的代码 jvm.cpp :
JVM_ENTRY(jint, JVM_IHashCode(JNIEnv* env, jobject handle))
JVMWrapper("JVM_IHashCode");
// as implemented in the classic virtual machine; return 0 if object is NULL
return handle == NULL ? 0 : ObjectSynchronizer::FastHashCode (THREAD, JNIHandles::resolve_non_null(handle)) ;
JVM_END
这里只是一个包装,实际计算 hashCode 的是 ObjectSynchronizer::FastHashCode
,位于 synchronizer.cpp :
intptr_t ObjectSynchronizer::FastHashCode (Thread * Self, oop obj) {
if (UseBiasedLocking) {
// NOTE: many places throughout the JVM do not expect a safepoint
// to be taken here, in particular most operations on perm gen
// objects. However, we only ever bias Java instances and all of
// the call sites of identity_hash that might revoke biases have
// been checked to make sure they can handle a safepoint. The
// added check of the bias pattern is to avoid useless calls to
// thread-local storage.
if (obj->mark()->has_bias_pattern()) {
// Box and unbox the raw reference just in case we cause a STW safepoint.
Handle hobj (Self, obj) ;
// Relaxing assertion for bug 6320749.
assert (Universe::verify_in_progress() ||
!SafepointSynchronize::is_at_safepoint(),
"biases should not be seen by VM thread here");
BiasedLocking::revoke_and_rebias(hobj, false, JavaThread::current());
obj = hobj() ;
assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
}
}
// hashCode() is a heap mutator ...
// Relaxing assertion for bug 6320749.
assert (Universe::verify_in_progress() ||
!SafepointSynchronize::is_at_safepoint(), "invariant") ;
assert (Universe::verify_in_progress() ||
Self->is_Java_thread() , "invariant") ;
assert (Universe::verify_in_progress() ||
((JavaThread *)Self)->thread_state() != _thread_blocked, "invariant") ;
ObjectMonitor* monitor = NULL;
markOop temp, test;
intptr_t hash;
markOop mark = ReadStableMark (obj);
// object should remain ineligible for biased locking
assert (!mark->has_bias_pattern(), "invariant") ;
if (mark->is_neutral()) {
hash = mark->hash(); // this is a normal header
if (hash) { // if it has hash, just return it
return hash;
}
hash = get_next_hash(Self, obj); // allocate a new hash code
temp = mark->copy_set_hash(hash); // merge the hash code into header
// use (machine word version) atomic operation to install the hash
test = (markOop) Atomic::cmpxchg_ptr(temp, obj->mark_addr(), mark);
if (test == mark) {
return hash;
}
// If atomic operation failed, we must inflate the header
// into heavy weight monitor. We could add more code here
// for fast path, but it does not worth the complexity.
} else if (mark->has_monitor()) {
monitor = mark->monitor();
temp = monitor->header();
assert (temp->is_neutral(), "invariant") ;
hash = temp->hash();
if (hash) {
return hash;
}
// Skip to the following code to reduce code size
} else if (Self->is_lock_owned((address)mark->locker())) {
temp = mark->displaced_mark_helper(); // this is a lightweight monitor owned
assert (temp->is_neutral(), "invariant") ;
hash = temp->hash(); // by current thread, check if the displaced
if (hash) { // header contains hash code
return hash;
}
// WARNING:
// The displaced header is strictly immutable.
// It can NOT be changed in ANY cases. So we have
// to inflate the header into heavyweight monitor
// even the current thread owns the lock. The reason
// is the BasicLock (stack slot) will be asynchronously
// read by other threads during the inflate() function.
// Any change to stack may not propagate to other threads
// correctly.
}
// Inflate the monitor to set hash code
monitor = ObjectSynchronizer::inflate(Self, obj);
// Load displaced header and check it has hash code
mark = monitor->header();
assert (mark->is_neutral(), "invariant") ;
hash = mark->hash(); // 取出缓存
if (hash == 0) {
hash = get_next_hash(Self, obj); // 实际计算
temp = mark->copy_set_hash(hash); // merge hash code into header
assert (temp->is_neutral(), "invariant") ;
test = (markOop) Atomic::cmpxchg_ptr(temp, monitor, mark);
if (test != mark) {
// The only update to the header in the monitor (outside GC)
// is install the hash code. If someone add new usage of
// displaced header, please update this code
hash = test->hash();
assert (test->is_neutral(), "invariant") ;
assert (hash != 0, "Trivial unexpected object/monitor header usage.");
}
}
// We finally get the hash
return hash;
}
又是假牙,实际计算 hashCode 的是 get_next_hash
,代码和 ObjectSynchronizer::FastHashCode
相邻:
// hashCode() generation :
//
// Possibilities:
// * MD5Digest of {obj,stwRandom}
// * CRC32 of {obj,stwRandom} or any linear-feedback shift register function.
// * A DES- or AES-style SBox[] mechanism
// * One of the Phi-based schemes, such as:
// 2654435761 = 2^32 * Phi (golden ratio)
// HashCodeValue = ((uintptr_t(obj) >> 3) * 2654435761) ^ GVars.stwRandom ;
// * A variation of Marsaglia's shift-xor RNG scheme.
// * (obj ^ stwRandom) is appealing, but can result
// in undesirable regularity in the hashCode values of adjacent objects
// (objects allocated back-to-back, in particular). This could potentially
// result in hashtable collisions and reduced hashtable efficiency.
// There are simple ways to "diffuse" the middle address bits over the
// generated hashCode values:
//
static inline intptr_t get_next_hash(Thread * Self, oop obj) {
intptr_t value = 0 ;
if (hashCode == 0) {
// This form uses an unguarded global Park-Miller RNG,
// so it's possible for two threads to race and generate the same RNG.
// On MP system we'll have lots of RW access to a global, so the
// mechanism induces lots of coherency traffic.
value = os::random() ; // 随机数
} else
if (hashCode == 1) {
// This variation has the property of being stable (idempotent)
// between STW operations. This can be useful in some of the 1-0
// synchronization schemes.
// 地址基础上 hack
intptr_t addrBits = intptr_t(obj) >> 3 ;
value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
} else
if (hashCode == 2) {
value = 1 ; // for sensitivity testing, 实际不会使用
} else
if (hashCode == 3) {
value = ++GVars.hcSequence ;
} else
if (hashCode == 4) {
value = intptr_t(obj) ; // 直接用地址
} else {
// Marsaglia's xor-shift scheme with thread-specific state
// This is probably the best overall implementation -- we'll
// likely make this the default in future releases.
unsigned t = Self->_hashStateX ;
t ^= (t << 11) ;
Self->_hashStateX = Self->_hashStateY ;
Self->_hashStateY = Self->_hashStateZ ;
Self->_hashStateZ = Self->_hashStateW ;
unsigned v = Self->_hashStateW ;
v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
Self->_hashStateW = v ;
value = v ;
}
value &= markOopDesc::hash_mask;
if (value == 0) value = 0xBAD ;
assert (value != markOopDesc::no_hash, "invariant") ;
TEVENT (hashCode: GENERATE) ;
return value;
}
这段代码可以看出 OpenJDK 一共实现了 5 中不同的计算 hash 值的方法,通过
这段代码中 hashCode
进行切换。其中 hashCode == 4
的是直接使用地址的(前面的实验说明 OpenJDK 默认情况下并没有使用这种方式,或许可以通过运行/编译时参数进行选择)。
结论
前面通过 JNI 验证已经能够得到很显然的结论,hashCode 返回的并不一定是对象的(虚拟)内存地址,具体取决于运行时库和 JVM 的具体实现。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

上一篇: 常见字符编码详解
下一篇: 谈谈自己对于 AOP 的了解
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论