String 類的intern
方法可能你們比較少用也比較陌生,雖然實際項目中並不太建議使用intern
方法,能夠在 Java 層來實現相似的池,但咱們仍是要知道它的原理機制不是。java
經過該方法能夠返回一個字符串標準對象,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";
}
}
複製代碼
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;
}
複製代碼
邏輯以下,
java_lang_String::hash_code
獲得哈希值。lookup_shared
函數查找查看共享哈希表中是否已經有這個值的字符串對象,若是有則直接返回找到的對象,該函數會間接調用lookup
函數,後面會進一步分析。hash_to_index
函數計算哈希值對應的索引值。lookup_in_main_table
函數到對應索引值的桶內去查找字符串對象,若是找到就返回該對象。MutexLocker
加鎖,而後調用basic_add
函數完成添加操做,該函數後面會進一步分析。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);
複製代碼
查找哈希表的邏輯爲,
VALUE_ONLY_BUCKET_TYPE
類型的桶,則直接解碼偏移量對應的對象,該類型的 entries 中每一個 entry 只有一個4字節用來表示偏移量,即u4 offset;
。u4 hash;union {u4 offset; narrowOop str; }
,前面爲哈希值,後面爲偏移或字符對象指針。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;
}
複製代碼
添加哈希表的邏輯以下,
lookup_in_main_table
函數檢查哈希表中是否已經存在字符串值。add_entry
函數添加到哈希表中。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();
}
複製代碼
前面說了 JVM 默認的狀況下的哈希表的桶大小爲:64位系統爲 60013,而32位的則爲 1009。若是咱們要改變它的大小,能夠經過設置-XX:StringTableSize
來達到效果。
若是你想看常量池相關的統計,能夠設置-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
複製代碼
-------------推薦閱讀------------
------------------廣告時間----------------
跟我交流,向我提問:
公衆號的菜單已分爲「分佈式」、「機器學習」、「深度學習」、「NLP」、「Java深度」、「Java併發核心」、「JDK源碼」、「Tomcat內核」等,可能有一款適合你的胃口。
歡迎關注: