引言
字串常量池(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中的物件參考,如下圖所示:

執行程序如下:
- 執行第一行代碼時,發現"abc"不存在StringTable中,會在PermGen新建一個String物件,并回傳其參考
- 執行第二行代碼時,發現"abc"已經存在于StringTable中,會在Heap中新建一個String物件,并且這個物件會共享之前s1的value陣列
在JDK7中,StringTable被移動到Heap中,在執行第一行代碼時,創建"abc"字串也是在Heap中進行,看起來區別并不大,僅僅是從PermGen移動到了Heap中,但這一改動會影響intern()方法的執行邏輯,后面會具體解釋,

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里面確實是沒錯的,如下圖所示:

但是在JDK6里面,因為字串物件s1是直接通過傳入char陣列new出來的,這個String物件是在Heap上的,
而StringTable是在PermGen里面的,無法直接將s1放入StringTable,jvm會在PermGen創建一個新的String物件,再把這個新的String物件放入StringTable中,
所以后面String s2 = "abc"時,會通過StringTable回傳新的String物件給s2,因此此時結果為false,如下圖所示:

可以通過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找國內的鏡像,
參考資料
- 從字串到常量池,一文看懂String類
- 深入決議String#intern
- JEP 310: Application Class-Data Sharing
- JEP 341: Default CDS Archives
- Java12新特性 -- 默認生成類資料共享(CDS)歸檔檔案
- OpenJDK源代碼
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/274669.html
標籤:其他
上一篇:Paxos 協議簡單介紹
