剛學Java的時候我也有過這種懷疑,但一直沒有驗證;最近在OSCHINA上看到有人在回答問題時也這麼說,於是萌生了一探究竟的想法——java.lang.Object.hashCode()
的返回值到底是不是對象內存地址?
(順帶回顧一下JNI)
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:
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 文件。
Run in shell——在shell環境運行
下面就讓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);
}
讓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
All in one —— Makefile
上面的多個命令可以寫到一個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
幾個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的具體實現。
我的運行環境:
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)
歡迎評論或email([email protected])交流觀點,轉載註明出處,勿做商用。