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

超人汪小建發表於2018-04-09

前言

String 類的intern方法可能大家比較少用也比較陌生,雖然實際專案中並不太建議使用intern方法,可以在 Java 層來實現類似的池,但我們還是要知道它的原理機制不是。

關於intern方法

通過該方法可以返回一個字串標準物件,JVM 有一個專門的字串常量池來維護這些標準物件,常量池是一個雜湊 map 結構,字串物件呼叫intern方法會先檢查池中是否已經存在該字串的標準物件,如果存在則直接返回標準物件,如果不存在則會往池中建立標準物件並且返回該物件。

查詢過程是使用字串的值作為 key 進行的,也就是說對於相同的字串值獲取到的都是同一個標準物件,比如在 Java 層可以有多個字串值為“key1”的字串物件,但通過intern方法獲取到的都是同一個物件。

有什麼作用

那麼intern方法有什麼作用呢?前面我們知道了 Java 層只要字串的值相等那麼通過intern獲取到的一定是同一個物件,也就是所謂的標準物件。比如下面,

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方法就可以將字串物件數減少到一萬,值相同的都共用同一個標準物件。

加入執行時常量池

在 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 層的物件並返回。

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,大致如下圖所示,

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

主要邏輯是先計算 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核心設計剖析》

歡迎關注:

這裡寫圖片描述

相關文章