從HotSpot VM源碼看字符串常量池(StringTable)和intern()方法


引言

字符串常量池(StringTable)是JVM中一個重要的結構,它有助於避免重復創建相同內容的String對象。那么StringTable是怎么實現的?“把字符串加入到字符串常量池中”這個過程發生了?intern()方法又做了什么?上面的問題在JDK6和JDK7中又有什么不一樣的答案?

網絡上已經有海量的文章討論過上面這些問題,但是不同的文章會給出截然相反的結論。

比如:

  • StringTable中保存的是String對象,還是String對象的引用?
  • new String("a"),是在堆里創建一個新的值為“a"的String對象,還是創建一個指向StringTable中代表”a“的value數組的對象
  • new String("a") 和 字面量"a"產生的字符串對象,用的是不是同一個value數組?

想找到這些問題的准確答案,靠搜索引擎上面的資料實在太難了,還是直接看HotSpot VM的源代碼更方便一點。這也印證了Linus Torvalds的那句名言:

“Talk is cheap. Show me the code.”

源碼中StringTable的結構

StringTable的底層結構

字符串常量池可以簡單理解為就是一個hashmap的結構,記錄的是字符串序列和String對象引用的映射關系

hotspot\share\memory\universe.cpp中對StringTable進行了初始化:

StringTable::create_table();

可以看看create_table()函數的源碼,位於hotspot\share\classfile\stringTable.cpp

void StringTable::create_table() {
  size_t start_size_log_2 = ceil_log2(StringTableSize);
  _current_size = ((size_t)1) << start_size_log_2;
  log_trace(stringtable)("Start size: " SIZE_FORMAT " (" SIZE_FORMAT ")",
                         _current_size, start_size_log_2);
  _local_table = new StringTableHash(start_size_log_2, END_SIZE, REHASH_LEN);
  _oop_storage = OopStorageSet::create_weak("StringTable Weak");
  _oop_storage->register_num_dead_callback(&gc_notification);
}

里面最關鍵的是_local_table = new StringTableHash(start_size_log_2, END_SIZE, REHASH_LEN);

這一行代碼對_local_table進行了初始化,這里的_local_table是一個static類型的變量,指向的是StringTableHash類的對象。

StringTableHash是什么?

StringTableHash是個別名,它實際上是hotspot\share\utilities\concurrentHashTable.hpp中定義的ConcurrentHashTable。如下:

typedef ConcurrentHashTable<StringTableConfig, mtSymbol> StringTableHash;
static StringTableHash* _local_table = NULL;

ConcurrentHashTable的源碼就不貼出來了,里面有注釋說明它是A mostly concurrent-hash-table,簡單來說就是支持並發操作的hash表,類似於jdk中的ConcurrentHashMap。

讀到這里,可以得到以下信息:

  • StringTable只在universe.cpp中被初始化,之后都是共享的。
  • StringTable的底層是_local_table指向的ConcurrentHashTable,一個並發散列表。
  • StringTable的數據保存在一個靜態變量中,全局共享。

StringTable支持的操作

StringTable里面的函數全部是static類型的,這意味着它是一個提供靜態方法的類,是全局共享的。

下面是stringTable.hpp中定義的核心public函數列表:

public:
  static size_t table_size();
  static TableStatistics get_table_statistics();

  static void create_table();

  static void do_concurrent_work(JavaThread* jt);
  static bool has_work();

  // Probing
  static oop lookup(Symbol* symbol);
  static oop lookup(const jchar* chars, int length);

  // Interning
  static oop intern(Symbol* symbol, TRAPS);
  static oop intern(oop string, TRAPS);
  static oop intern(const char *utf8_string, TRAPS);

  // Rehash the string table if it gets out of balance
  static void rehash_table();
  static bool needs_rehashing() { return _needs_rehashing; }
  static inline void update_needs_rehash(bool rehash) {
    if (rehash) {
      _needs_rehashing = true;
    }
  }

從函數命名也可以看出StringTable主要支持的操作:

  • 創建,查看表信息和狀態等操作如table_size()create_table()has_work()get_table_statistics()
  • 查找字符串如lookup(),嘗試池化字符串如intern()
  • hash相關操作如rehash_table()needs_rehashing()

lookup()方法

對外部來說最關鍵的就是lookup()intern()方法,intern()后面會再解釋。這里先看看lookup()

lookup就是查找的意思,用於通過字符串查找對應的String對象。最終會執行到do_lookup()方法:

oop StringTable::do_lookup(const jchar* name, int len, uintx hash) {
  Thread* thread = Thread::current();
  StringTableLookupJchar lookup(thread, hash, name, len);
  StringTableGet stg(thread);
  bool rehash_warning;
  _local_table->get(thread, lookup, stg, &rehash_warning);
  update_needs_rehash(rehash_warning);
  return stg.get_res_oop();
}

這里可以看到這樣一行代碼: _local_table->get(thread, lookup, stg, &rehash_warning);

說明String對象最終是從_local_table中拿到的,返回值類型是oop也就是普通對象引用。

類數據共享(Class-Data Sharing)

從StringTable的另外一個Map說起

前面說到StringTable的底層是_local_table指向的concurrentHashTable。但我看的StringTable源碼中(JDK16),還有另外一個Map:

static CompactHashtable<
  const jchar*, oop,
  read_string_from_compact_hashtable,
  java_lang_String::equals
> _shared_table;

這里定義了一個CompactHashtable類型的變量_shared_table。並且有一些專門為其提供的方法:

  // Sharing
 private:
  static oop lookup_shared(const jchar* name, int len, unsigned int hash) NOT_CDS_JAVA_HEAP_RETURN_(NULL);
 public:
  static oop create_archived_string(oop s, Thread* THREAD) NOT_CDS_JAVA_HEAP_RETURN_(NULL);
  static void shared_oops_do(OopClosure* f) NOT_CDS_JAVA_HEAP_RETURN;
  static void write_to_archive(const DumpedInternedStrings* dumped_interned_strings) NOT_CDS_JAVA_HEAP_RETURN;
  static void serialize_shared_table_header(SerializeClosure* soc) NOT_CDS_JAVA_HEAP_RETURN;

  // Jcmd
  static void dump(outputStream* st, bool verbose=false);
  // Debugging
  static size_t verify_and_compare_entries();
  static void verify();

因此去看了一下源碼

_compact_buckets = MetaspaceShared::new_ro_array<u4>(_num_buckets + 1);
_compact_entries = MetaspaceShared::new_ro_array<u4>(entries_space);

它是通過MetaspaceShared::new_ro_array來申請空間。ro表示了它是塊只讀的內存空間。

MetaspaceShared的源碼注釋中提到,它提供三種類型的空間分配:

// The CDS archive is divided into the following regions:
//     mc  - misc code (the method entry trampolines, c++ vtables)
//     rw  - read-write metadata
//     ro  - read-only metadata and read-only tables

並且這三塊空間在內存中是連續的。

看起來很奇怪,已經有了_local_table,為什么還需要用一個只讀的空間來保存字符串?

而且Metaspace在JDK1.8中已經移動到本地內存中了,而字符串常量池此時是在堆中?

這就要提到下面的類數據共享了。

類數據共享的發展歷史

下面的歷史引自博客:Java12新特性 -- 默認生成類數據共享(CDS)歸檔文件

  • JDK5引入了Class-Data Sharing可以用於多個JVM共享class,提升啟動速度,最早只支持system classes及serial GC。
  • JDK9對其進行擴展以支持application classes及其他GC算法。
  • java10的新特性JEP 310: Application Class-Data Sharing擴展了JDK5引入的Class-Data Sharing,支持application的Class-Data Sharing並開源出來(以前是commercial feature)
    • CDS 只能作用於 BootClassLoader 加載的類,不能作用於 AppClassLoader 或者自定義的 ClassLoader加載的類。在 Java 10 中,則將 CDS 擴展為 AppCDS,顧名思義,AppCDS 不止能夠作用於BootClassLoader了,AppClassLoader 和自定義的 ClassLoader 也都能夠起作用,大大加大了 CDS 的適用范圍。也就說開發自定義的類也可以裝載給多個JVM共享了
  • JDK11將-Xshare:off改為默認-Xshare:auto,以更加方便使用CDS特性。

Java 10的Application Class-Data Sharing

Java 10中引入了Application Class-Data Sharing。在JEP 310中做了簡單說明:

JEP 310: Application Class-Data Sharing
Summary
	To improve startup and footprint, extend the existing Class-Data Sharing ("CDS") feature to allow application classes to 	be placed in the shared archive.
Goals
- Reduce footprint by sharing common class metadata across different Java processes.
- Improve startup time.
- Extend CDS to allow archived classes from the JDK's run-time image file ($JAVA_HOME/lib/modules) and the application class   path to be loaded into the built-in platform and system class loaders.
- Extend CDS to allow archived classes to be loaded into custom class loaders.

網上似乎沒有多少資料談到這個類數據共享機制,不過從這個草案也可以略知一二:

  • Class-Data Sharing 允許將Java類放置在共享的存檔空間中
  • 通過在不同的Java進程之間共享公共類元數據來減少內存占用

這也就可以解釋上文提到的_shared_table的用處:用於在不同的Java進程之間共享字符串池。

StringTable和intern()方法的變化

StringTable在JDK1.7的變化

把String對象加入StringTable的邏輯是:

  • 從 StringTable 中找給定的字符串對象,找到的話就直接返回其引用
  • 找不到就把當前字符串對象添加到 StringTable 中,然后返回引用

接下來以下面的代碼執行過程為例說明StringTable在JDK6和JDK7中的區別:

String s1 = "abc";
String s2 = new String("abc");

在JDK6及以前,StringTable在PermGen中,字符串常量池中保存的也是PermGen中的對象引用,如下圖所示:

image-20210205234423774

執行過程如下:

  • 執行第一行代碼時,發現"abc"不存在StringTable中,會在PermGen新建一個String對象,並返回其引用
  • 執行第二行代碼時,發現"abc"已經存在於StringTable中,會在Heap中新建一個String對象,並且這個對象會共享之前s1的value數組

在JDK7中,StringTable被移動到Heap中。在執行第一行代碼時,創建"abc"字符串也是在Heap中進行。看起來區別並不大,僅僅是從PermGen移動到了Heap中,但這一改動會影響intern()方法的執行邏輯,后面會具體解釋。

image-20210205235109679

intern()方法在JDK1.7的變化

String Table在JDK1.6中位於Perm Gen,但是在JDK1.7中被轉移到了Java Heap中,這次轉移伴隨着String.intern()方法的性質發生了一些微小的改變。

  • 在1.6中,intern的處理是先判斷字符串常量是否在字符串常量池中,如果存在直接返回該對象的引用。如果沒有找到,則將該字符串常量加入到字符串常量區,也就是在永久代中創建該字符串對象,再把引用保存到字符串常量池中。
  • 在1.7中,intern的處理是先判斷字符串常量是否在字符串常量池中,如果存在直接返回該對象的引用,如果沒有找到,說明該字符串常量在堆中,則處理是把堆區該對象的引用加入到字符串常量池中,以后別人拿到的是該字符串常量的引用,實際存在堆中。

例如下面的代碼:

String s1 = new String(new char[]{'a','b','c'});  
s1.intern();  
String s2 = "abc";
System.out.println(s1 == s2);  

按照常規的思路,s1.intern()會將s1放進字符串常量池,然后String s2 = "abc"時,會通過StringTable返回s1的引用給s2,所以結果是true。

這在JDK7里面確實是沒錯的,如下圖所示:

image-20210206000048585

但是在JDK6里面,因為字符串對象s1是直接通過傳入char數組new出來的,這個String對象是在Heap上的。

而StringTable是在PermGen里面的,無法直接將s1放入StringTable,jvm會在PermGen創建一個新的String對象,再把這個新的String對象放入StringTable中。

所以后面String s2 = "abc"時,會通過StringTable返回新的String對象給s2,因此此時結果為false,如下圖所示:

image-20210206000711788

可以通過JDK6和JDK7中intern()的C++源碼來驗證:

JDK 6 版本的 openjdk 代碼:

// try to reuse the string if possible
  if (!string_or_null.is_null() && (!JavaObjectsInPerm || string_or_null()->is_perm())) {
    string = string_or_null;
  } else {
    string = java_lang_String::create_tenured_from_unicode(name, len, CHECK_NULL);
  }

JDK 7 版本的 openjdk 代碼:

// try to reuse the string if possible
  if (!string_or_null.is_null()) {
    string = string_or_null;
  } else {
    string = java_lang_String::create_from_unicode(name, len, CHECK_NULL);
  }

區別在JDK6在把字符串放入StringTable時多了一行判斷:

 (!JavaObjectsInPerm || string_or_null()->is_perm())
  • 這個用於判斷字符串是否在永久代中,如果是,最終會將這個 string_or_null 放入 StringTable 中
  • 否則,最終會通過java_lang_String::create_tenured_from_unicode在永久代中再次創建一個 String 對象,然后放入 StringTable 中。

結語

在HotSpot VM的源碼中主要得到了下面的信息:

  • 字符串常量池可以簡單理解為就是一個hashmap的結構,記錄的是字符串序列和String對象引用的映射關系
  • 為了在不同的Java進程之間共享字符串池,StringTable還有另外一個名為_shared_table的Map
  • JDK6中,會在永久代創建String對象再放入StringTable,而在JDK7中則直接將堆中的String對象放入StringTable中

OpenJDK中包含HotSpot VM的源碼,是完全開源的。感興趣的可以自行下載閱讀:OpenJDK源代碼

如果嫌Github下載太慢也可以去Gitee找國內的鏡像。

參考資料


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM