引言
字符串常量池(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找國內的鏡像。