刚学Java的时候我也有过这种怀疑,但一直没有验证;最近在OSCHINA上看到有人在回答问题时也这么说,于是萌生了一探究竟的想法——java.lang.Object.hashCode()
的返回值到底是不是对象内存地址?
(顺带回顾一下JNI)
说到这个问题,大家的第一反应一定和我一样——去查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
的具体实现作简要分析。
看不到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字节,足矣~
假设我们已经有了一个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));
}
}
好了说干就干,现在就差那个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:
user@host:/usr/lib/jvm/java-7-openjdk-amd64$ 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 文件。
下面就让TestNativeUtils
在shell环境执行起来。
首先,编译NativeUtils
和TestNativeUtils
:
$ javac NativeUtils.java
$ javac TestNativeUtils.java
分别生成了NativeUtils.class和TestNativeUtils.class
好了,就差临门一脚了——执行class文件:
$ java TestNativeUtils
居然出错了:
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);
}
再次编译、执行:
$ 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
上面的多个命令可以写到一个Makefile里,可以实现“一键执行”:
test: runtest
all: libnative-utils.so
JNI_INCLUDE=/usr/lib/jvm/java-7-openjdk-amd64/include
NativeUtils.class: NativeUtils.java
javac NativeUtils.java
TestNativeUtils.class: TestNativeUtils.java
javac TestNativeUtils.java
NativeUtils.h: NativeUtils.java
javah -jni NativeUtils
libnative-utils.so: NativeUtils.cc NativeUtils.h
g++ -shared -I${JNI_INCLUDE} -o libnative-utils.so NativeUtils.cc
runtest: TestNativeUtils.class libnative-utils.so
@echo "run test:"
java -Djava.library.path=. TestNativeUtils
clean:
rm -v *.class *.so *.h
说道开源的Java运行环境,首先能想到的自然是OpenJDK和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
的返回值和对象的地址并没有直接的关系。
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的具体实现。
我的运行环境:
OS:
Ubuntu 12.04 64bit Desktop |
JDK:
java version “1.7.0_55”
OpenJDK Runtime Environment (IcedTea 2.4.7) (7u55-2.4.7-1ubuntu1~0.12.04.2) \
OpenJDK 64-Bit Server VM (build 24.51-b03, mixed mode)
java.lang.Object.hashCode()的返回值到底是不是对象内存地址?
原文地址:http://blog.csdn.net/xusiwei1236/article/details/45152201