細說 Java hashCode

作者:碼匠筆記
來源:碼匠筆記,id:majiangbiji


細說 Java hashCode

前言

寫過 Java 程序的同學一定都知道 hashCode 方法,它是 Object 對象的一個 native 方法。無論是我們平常使用的 HashMap 還是重寫 equals 方法的時候,都會接觸到 hashCode 方法,那麼它究竟是怎麼生成的,又有什麼作用呢?筆者帶著這個疑問開始探尋。

hashCode 方法的定義

在 jdk api 中 關於 hashCode 有如下說明:

Returns a hash code value for the object.
This method is supported for the benefit of hash tables such as those provided by HashMap.
The general contract of hashCode is:
Whenever it is invoked on the same object more than once during an execution of a Java application,
the hashCode method must consistently return the same integer,
provided no information used in 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.
If two objects are equal according to the equals(Object) method,
then calling the hashCode method on each of the two objects must produce the same integer result.
It is not required that if two objects are unequal according to the equals(java.lang.Object) method,
then calling the 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.
As much as is reasonably practical,
the hashCode method defined by class 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 JavaTM programming language.)

其大致意思如下

只要在Java應用程序的執行過程中多次調用同一個對象,
hashCode方法必須始終返回相同的整數,
前提是在對象的equals比較中沒有使用的信息被修改。
從應用程序的一次執行到同一應用程序的另一次執行,此整數不必保持一致。

如果兩個對象按照equals(Object)方法相等,
那麼在兩個對象的每一個上調用hashCode方法必須產生相同的整數結果。
如果兩個對象根據equals(java.lang.Object)方法不相等,
則不要求對兩個對象中的每個對象調用hashCode方法都必須產生不同的整數結果。
但是,程序員應該知道,為不相等的對象生成不同的整數結果可以提高散列表的性能。
儘可能多地合理實用,由類Object定義的hashCode方法確實為不同的對象返回不同的整數。
這通常通過將對象的內部地址轉換為整數來實現,但JavaTM編程語言不需要此實現技術。

所以由上可以得到兩條有用的信息,同一個對象 hashcode 的值在一次運行中一定相等,並且不同對象的 hashcode 一定不同,但是他還備註通常使用內部地址轉換,但是 JAVA 不是使用這種方式實現的,那麼怎麼實現的呢?

hashCode 實現原理

hashcode 源碼

OpenJDK 的源碼可以直接查看,所以我們就選擇查看一下其源碼一看究竟。

我們可以看到 src/share/vm/prims/jvm.h和 src/share/vm/prims/jvm.cpp兩個文件中有關於 hashcode 的說明如下:

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

我們繼續進入 FashHashCode裡面查看,其位於 src/share/vm/runtime/synchronizer.cpp文件,相對代碼比較多,我們只摘取關鍵部分:

細說 Java hashCode


monitor 相關代碼我們先略過不理,通過 if 語句我們可以看出,當 hash為0時候需要調用 get_next_hash 生成一個新的 hash,那麼我們便可以繼續前行。

細說 Java hashCode

細說 Java hashCode


通過上述代碼我們看到,其實 hashCode 的生成有6中方式

  1. 隨機數
  2. 對象的內存地址的函數
  3. 固定值,這個只是為了進行靈敏度測試
  4. 遞增序列
  5. int類型的該對象的內存地址
  6. 結合當前線程和xorshift生成

通過 globals.hpp 我們可以發現,JDK8 默認為5,也就是最後一種。

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

當然,OpenJDK6,7中用的都是第一種方案,那麼問題又來了,既然都是隨機數,那麼怎麼確保每次都一樣的呢?

對象頭

這裡就需要引入一個 對象頭的概念,每次對象生成以後,都需要找一個地方存儲一下這個對象的hashCode和鎖信息,這就是 對象頭,英文稱之為 MarkWord。這樣一來我們就明白了,每次生成對象以後都會把它的 hashCode存起來,這樣無論對象怎麼在新生代,老年代之間 遊走都不會改變其 hashCode的值,然而事實並沒有那麼簡單。

偏向鎖

這時候我們翻回來看剛才略過的內容, ObjectSynchronizer::FastHashCode()裡面的其他邏輯。

細說 Java hashCode


由上述代碼我們可以得知,當前對象處於 偏向鎖時,會清除 偏向鎖通過從 鎖上面取回 MarkWord 信息。為什麼提到取回呢?之前消失了嗎?是的,現在就需要解釋一下 偏向鎖了。

Hotspot 的作者經過以往的研究發現大多數情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入

了偏向鎖。當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裡存儲鎖偏向的線程 ID,以後該線程在進入和退出同步塊時不需要花費 CAS 操作來加鎖和解鎖,而只需簡單的測試一下對象頭的 MarkWord 裡是否存儲著指向當前線程的偏向鎖,如果測試成功,表示線程已經獲得了鎖,如果測試失敗,則需要再測試下 MarkWord 中偏向鎖的標識是否設置成 1(表示當前是偏向鎖),如果沒有設置,則使用 CAS 競爭鎖,如果設置了,則嘗試使用 CAS 將對象頭的偏向鎖指向當前線程。所以我們便知道為什麼有 取回這個概念了。然而代碼帶沒有結束。

輕量級鎖

輕量級鎖相對比較簡單, JVM會在當前的線程棧楨中創建用於存放鎖的空間,同時將對象頭中的 MarkWord複製到鎖記錄中,也稱作 DisplacedMarkWord。比較複雜的是 重量級鎖。

重量級鎖

這個時候如果多個線程來競爭資源,就會發生 鎖膨脹,這樣因為需要保存競爭資源需要 wait的線程和相關信息,就引入了 monitor的概念。於是這時候就把 MarkWord存放到了 Monitor裡面,當然 Monitor不僅僅用於存儲對象的 MarkWord,具體的作用就不是本文的重點了。

hashCode 的用途

hashCode 的唯一性決定了他可以用來生成 HashMap的key,同時也能判斷對象是否為同一個對象。另外我們再重寫他的時候要多加註意,因為 JVM會根據它做一些性能優化。

總結

此文為筆者學習 hashCode 的筆記,如有問題歡迎指正。

參考文獻
OpenJDK 源碼
Oracle JDK Docs


分享到:


相關文章: