深刻談談String.intern()在JVM的實現

前言

String 類的intern方法可能你們比較少用也比較陌生,雖然實際項目中並不太建議使用intern方法,能夠在 Java 層來實現相似的池,但咱們仍是要知道它的原理機制不是。java

關於intern方法

經過該方法能夠返回一個字符串標準對象,JVM 有一個專門的字符串常量池來維護這些標準對象,常量池是一個哈希 map 結構,字符串對象調用intern方法會先檢查池中是否已經存在該字符串的標準對象,若是存在則直接返回標準對象,若是不存在則會往池中建立標準對象而且返回該對象。算法

查找過程是使用字符串的值做爲 key 進行的,也就是說對於相同的字符串值獲取到的都是同一個標準對象,好比在 Java 層能夠有多個字符串值爲「key1」的字符串對象,但經過intern方法獲取到的都是同一個對象。數組

有什麼做用

那麼intern方法有什麼做用呢?前面咱們知道了 Java 層只要字符串的值相等那麼經過intern獲取到的必定是同一個對象,也就是所謂的標準對象。好比下面,bash

String st = new String("hello world");
String st2 = new String("hello world");
System.out.println(st.intern() == st2.intern());
複製代碼

發現了嗎?咱們居然能用==來對比兩個對象的值了,要知道在 Java 中這樣比較只能判斷它們是否爲同一個引用的,但經過intern方法處理後就能夠直接這樣對比了,比起equals但是快不少啊,性能蹭蹭漲。你可能會說是啊,那是由於intern已經作了相似equals的比較操做了啊,這裏照樣會很耗時的好嘛!是的,你說的沒錯,但假如我後面要進行屢次比較,那是否是就體現出優點來了,只要作一次equals後面比較所有均可以用==進行快速比較了。併發

另外,某些場景下也能達到節省內存的效果,好比要維護大量且可能重複的字符串對象,好比十萬個字符串對象,而字符串值相同的有九萬個,那麼經過intern方法就能夠將字符串對象數減小到一萬,值相同的都共用同一個標準對象。app

加入運行時常量池

在 Java 層有兩種方式能將字符串對象加入到運行時常量池中:機器學習

  • 在程序中直接使用雙引號來聲明字符串對象,執行時該對象會被加入到常量池。好比下面,該類被編譯成字節碼後在運行時有相應的指令會將其添加到常量池中。
public class Test{
    public static void main(String[] args){
        String s = "hello";
    }
}
複製代碼
  • 另一種是經過 String 類的intern方法,它能檢測常量池中是否已經有當前字符串存在,若是不存在則將其加入到常量池中。好比下面,
String s = new String("hello");
s.intern();
複製代碼

再來個例子

JDK9。分佈式

public class Test {
	public static void main(String[] args) {
		String s = new String("hello");
		String ss = new String("hello");
		System.out.println(ss == s);
		String sss = s.intern();
		System.out.println(sss == s);
		String ssss = ss.intern();
		System.out.println(ssss == sss);

		System.out.println("=========");

		String s2 = "hello2";
		String ss2 = new String("hello2");
		System.out.println(ss2 == s2);
		String sss2 = s2.intern();
		System.out.println(sss2 == s2);
		String ssss2 = ss2.intern();
		System.out.println(ssss2 == sss2);
	}
}
複製代碼
false
false
true
=========
false
true
true
複製代碼

常量池的實現

Java 層很簡單,僅僅將intern定義爲本地方法。函數

public native String intern();
複製代碼

對應爲JVM_InternString函數,主要先經過JNIHandles::resolve_non_null函數轉成 JVM 層的 oop 指針,再調StringTable::intern函數得到最終返回的對象,最後再經過JNIHandles::make_local轉換成 Java 層的對象並返回。oop

JNIEXPORT jobject JNICALL
Java_java_lang_String_intern(JNIEnv *env, jobject this)
{
    return JVM_InternString(env, this);
}

JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str))
  JVMWrapper("JVM_InternString");
  JvmtiVMObjectAllocEventCollector oam;
  if (str == NULL) return NULL;
  oop string = JNIHandles::resolve_non_null(str);
  oop result = StringTable::intern(string, CHECK_NULL);
  return (jstring) JNIHandles::make_local(env, result);
JVM_END
複製代碼

主要看StringTable::intern,StringTable 就是 JVM 運行時用來存放常量的常量池。它的結構爲一個哈希 Map,大體以下圖所示,

主要邏輯是先計算 utf-8 編碼的字符串對應的 unicode 編碼的長度,按照 unicode 編碼所需的長度建立新的數組並將字符串轉換成 unicode 編碼,最後再調另一個intern函數。

oop StringTable::intern(const char* utf8_string, TRAPS) {
  if (utf8_string == NULL) return NULL;
  ResourceMark rm(THREAD);
  int length = UTF8::unicode_length(utf8_string);
  jchar* chars = NEW_RESOURCE_ARRAY(jchar, length);
  UTF8::convert_to_unicode(utf8_string, chars, length);
  Handle string;
  oop result = intern(string, chars, length, CHECK_NULL);
  return result;
}
複製代碼

邏輯以下,

  1. 經過java_lang_String::hash_code獲得哈希值。
  2. 根據哈希值調用lookup_shared函數查找查看共享哈希表中是否已經有這個值的字符串對象,若是有則直接返回找到的對象,該函數會間接調用lookup函數,後面會進一步分析。
  3. 是否使用了其餘哈希算法,是的話從新計算哈希值。
  4. 經過hash_to_index函數計算哈希值對應的索引值。
  5. 經過lookup_in_main_table函數到對應索引值的桶內去查找字符串對象,若是找到就返回該對象。
  6. 以上都沒在哈希表中找到的話則須要添加到表中了,用MutexLocker加鎖,而後調用basic_add函數完成添加操做,該函數後面會進一步分析。
  7. 返回字符串對象。
oop StringTable::intern(Handle string_or_null, jchar* name,
                        int len, TRAPS) {
  unsigned int hashValue = java_lang_String::hash_code(name, len);
  oop found_string = lookup_shared(name, len, hashValue);
  if (found_string != NULL) {
    return found_string;
  }
  if (use_alternate_hashcode()) {
    hashValue = alt_hash_string(name, len);
  }
  int index = the_table()->hash_to_index(hashValue);
  found_string = the_table()->lookup_in_main_table(index, name, len, hashValue);

  if (found_string != NULL) {
    if (found_string != string_or_null()) {
      ensure_string_alive(found_string);
    }
    return found_string;
  }
  Handle string;
  if (!string_or_null.is_null()) {
    string = string_or_null;
  } else {
    string = java_lang_String::create_from_unicode(name, len, CHECK_NULL);
  }
  oop added_or_found;
  {
    MutexLocker ml(StringTable_lock, THREAD);
    added_or_found = the_table()->basic_add(index, string, name, len,
                                  hashValue, CHECK_NULL);
  }

  if (added_or_found != string()) {
    ensure_string_alive(added_or_found);
  }

  return added_or_found;
}
複製代碼

常量池是一個哈希表,那麼它默認的桶的數量是多少呢?看下面的定義,64位系統上默認爲 60013,而32位的則爲 1009。

const int defaultStringTableSize = NOT_LP64(1009) LP64_ONLY(60013);
複製代碼

查找哈希表的邏輯爲,

  1. 哈希值對桶數取餘獲得索引。
  2. 經過索引獲取對應的桶信息。
  3. 獲取桶的偏移。
  4. 獲取桶的類型。
  5. 獲取 entry。
  6. 若是是VALUE_ONLY_BUCKET_TYPE類型的桶,則直接解碼偏移量對應的對象,該類型的 entries 中每一個 entry 只有一個4字節用來表示偏移量,即u4 offset;
  7. 若是是普通類型的桶,則遍歷 entry 找到指定哈希值的 entry 對應的偏移量,而後解碼偏移量對應的對象。其中entries 中每一個 entry 有8個字節,結構爲u4 hash;union {u4 offset; narrowOop str; },前面爲哈希值,後面爲偏移或字符對象指針。
  8. 兩種不一樣類型的結構能夠由如下簡單展現,第一個桶和第三個桶是普通類型,指向[哈希+偏移量]組成的不少 entry ,而第二個桶是VALUE_ONLY_BUCKET_TYPE類型,直接指向[偏移量]。
buckets[0, 4, 5, ....]
        |  |  |
        |  |  +---+
        |  |      |
        |  +----+ |
        v       v v
entries[H,O,H,O,O,H,O,H,O.....]
複製代碼
template <class T, class N>
inline T CompactHashtable<T,N>::lookup(const N* name, unsigned int hash, int len) {
  if (_entry_count > 0) {
    int index = hash % _bucket_count;
    u4 bucket_info = _buckets[index];
    u4 bucket_offset = BUCKET_OFFSET(bucket_info);
    int bucket_type = BUCKET_TYPE(bucket_info);
    u4* entry = _entries + bucket_offset;

    if (bucket_type == VALUE_ONLY_BUCKET_TYPE) {
      T res = decode_entry(this, entry[0], name, len);
      if (res != NULL) {
        return res;
      }
    } else {
      u4* entry_max = _entries + BUCKET_OFFSET(_buckets[index + 1]);
      while (entry < entry_max) {
        unsigned int h = (unsigned int)(entry[0]);
        if (h == hash) {
          T res = decode_entry(this, entry[1], name, len);
          if (res != NULL) {
            return res;
          }
        }
        entry += 2;
      }
    }
  }
  return NULL;
}
複製代碼

添加哈希表的邏輯以下,

  1. 是否使用了其餘哈希算法,是則從新計算哈希值,並計算對應的索引值。
  2. 經過lookup_in_main_table函數檢查哈希表中是否已經存在字符串值。
  3. 建立 entry ,包括了哈希值和字符串對象指針。
  4. 經過add_entry函數添加到哈希表中。
  5. 返回字符串對象。
oop StringTable::basic_add(int index_arg, Handle string, jchar* name,
                           int len, unsigned int hashValue_arg, TRAPS) {

  NoSafepointVerifier nsv;
  unsigned int hashValue;
  int index;
  if (use_alternate_hashcode()) {
    hashValue = alt_hash_string(name, len);
    index = hash_to_index(hashValue);
  } else {
    hashValue = hashValue_arg;
    index = index_arg;
  }
  oop test = lookup_in_main_table(index, name, len, hashValue); 
  if (test != NULL) {
    return test;
  }
  HashtableEntry<oop, mtSymbol>* entry = new_entry(hashValue, string());
  add_entry(index, entry);
  return string();
}
複製代碼

-XX:StringTableSize

前面說了 JVM 默認的狀況下的哈希表的桶大小爲:64位系統爲 60013,而32位的則爲 1009。若是咱們要改變它的大小,能夠經過設置-XX:StringTableSize來達到效果。

-XX:+PrintStringTableStatistics

若是你想看常量池相關的統計,能夠設置-XX:+PrintStringTableStatistics,那麼 JVM 中止時就會輸出相關信息了。好比,

SymbolTable statistics:
Number of buckets       :     20011 =    160088 bytes, avg   8.000
Number of entries       :     20067 =    481608 bytes, avg  24.000
Number of literals      :     20067 =    838520 bytes, avg  41.786
Total footprint         :           =   1480216 bytes
Average bucket size     :     1.003
Variance of bucket size :     0.994
Std. dev. of bucket size:     0.997
Maximum bucket size     :         8
StringTable statistics:
Number of buckets       :     60013 =    480104 bytes, avg   8.000
Number of entries       :   1003077 =  24073848 bytes, avg  24.000
Number of literals      :   1003077 =  48272808 bytes, avg  48.125
Total footprint         :           =  72826760 bytes
Average bucket size     :    16.714
Variance of bucket size :     9.683
Std. dev. of bucket size:     3.112
Maximum bucket size     :        30
複製代碼

-------------推薦閱讀------------

個人2017文章彙總——機器學習篇

個人2017文章彙總——Java及中間件

個人2017文章彙總——深度學習篇

個人2017文章彙總——JDK源碼篇

個人2017文章彙總——天然語言處理篇

個人2017文章彙總——Java併發篇

------------------廣告時間----------------

跟我交流,向我提問:

這裏寫圖片描述

公衆號的菜單已分爲「分佈式」、「機器學習」、「深度學習」、「NLP」、「Java深度」、「Java併發核心」、「JDK源碼」、「Tomcat內核」等,可能有一款適合你的胃口。

爲何寫《Tomcat內核設計剖析》

歡迎關注:

這裏寫圖片描述
相關文章
相關標籤/搜索