JVM源碼分析之String.intern()導致的YGC不斷變長
概述
之所以想寫這篇文章,是因為YGC過程對我們來說太過於黑盒,如果對YGC過程不是很熟悉,這類問題基本很難定位,我們就算開了GC日誌,也最多能看到類似下麵的日誌
[GC (Allocation Failure) [ParNew: 91807K->10240K(92160K), 0.0538384 secs] 91807K->21262K(2086912K), 0.0538680 secs] [Times: user=0.16 sys=0.06, real=0.06 secs]
隻知道耗了多長時間,但是具體耗在了哪個階段,是基本看不出來的,所以要麼就是靠經驗來定位,要麼就是對代碼相當熟悉,腦袋裏過一遍整個過程,看哪個階段最可能,今天要講的這個大家可以當做今後排查這類問題的一個經驗來使,這個當然不是唯一導致YGC過長的一個原因,但卻是最近我幫忙定位碰到的發生相對來說比較多的一個場景
具體的定位是通過在JVM代碼裏進行了日誌埋點確定的,這個問題其實最早的時候,是幫助畢玄畢大師定位到這塊的問題,他也在公眾號裏對這個問題寫了相關的一篇文章YGC越來越慢,為什麼,大家可以關注下畢大師的公眾號HelloJava
,經常會發一些在公司碰到的詭異問題的排查,相信會讓你漲姿勢的,當然如果你還沒有關注我的公眾號你假笨
,歡迎關注下,後續會時不時寫點或許正巧你感興趣的JVM係列文章。
Demo
先上一個demo,來描述下問題的情況,代碼很簡單,就是不斷創建UUID,其實就是一個字符串,並將這個字符串調用下intern方法
import java.util.UUID;public class StringTableTest { public static void main(String args[]) { for (int i = 0; i < 10000000; i++) {
uuid();
}
} public static void uuid() {
UUID.randomUUID().toString().intern();
}
}
我們使用的JVM參數如下:
-XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xmx2G -Xms2G -Xmn100M
這裏特意將新生代設置比較小,老生代設置比較大,讓代碼在執行過程中更容易突出問題來,大量做ygc,期間不做CMS GC,於是我們得到的輸出結果類似下麵的
有沒有發現YGC不斷發生,並且發生的時間不斷在增長,從10ms慢慢增長到了40ms,甚至還會繼續漲下去
String.intern方法
從上麵的demo我們能挖掘到的可能就是intern這個方法了,那我們先來了解下intern方法的實現,這是String提供的一個方法,jvm提供這個方法的目的是希望對於某個同名字符串使用非常多的場景,在jvm裏隻保留一份,比如我們不斷new String(“a”),其實在java heap裏會有多個String的對象,並且值都是a,如果我們隻希望內存裏隻保留一個a,或者希望我接下來用到的地方都返回同一個a,那就可以用String.intern這個方法了,用法如下
String a = "a".intern();
...
String b = a.intern();
這樣b和a都是指向內存裏的同一個String對象,那JVM裏到底怎麼做到的呢?
我們看到intern這個方法其實是一個native方法,具體對應到JVM裏的邏輯是
oop StringTable::intern(oop string, TRAPS)
{ if (string == NULL) return NULL; ResourceMark rm(THREAD); int length; Handle h_string (THREAD, string);
jchar* chars = java_lang_String::as_unicode_string(string, length);
oop result = intern(h_string, chars, length, CHECK_NULL); return result;
}
oop StringTable::intern(Handle string_or_null, jchar* name, int len, TRAPS) {
unsigned int hashValue = hash_string(name, len); int index = the_table()->hash_to_index(hashValue);
oop found_string = the_table()->lookup(index, name, len, hashValue); // Found
if (found_string != NULL) return found_string;
debug_only(StableMemoryChecker smc(name, len * sizeof(name[0]))); assert(!Universe::heap()->is_in_reserved(name) || GC_locker::is_active(), "proposed name of symbol must be stable");
Handle string; // 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);
} // Grab the StringTable_lock before getting the_table() because it could
// change at safepoint.
MutexLocker ml(StringTable_lock, THREAD); // Otherwise, add to symbol to table
return the_table()->basic_add(index, string, name, len,
hashValue, CHECK_NULL);
}
也就是說是其實在JVM裏存在一個叫做StringTable的數據結構,這個數據結構是一個Hashtable,在我們調用String.intern的時候其實就是先去這個StringTable裏查找是否存在一個同名的項,如果存在就直接返回對應的對象,否則就往這個table裏插入一項,指向這個String對象,那麼再下次通過intern再來訪問同名的String對象的時候,就會返回上次插入的這一項指向的String對象
至此大家應該知道其原理了,另外我這裏還想說個題外話,記得幾年前tomcat裏爆發的一個HashMap導致的hash碰撞的問題,這裏其實也是一個Hashtable,所以也還是存在類似的風險,不過JVM裏提供一個參數專門來控製這個table的size,-XX:StringTableSize
,這個參數的默認值如下
product(uintx, StringTableSize, NOT_LP64(1009) LP64_ONLY(60013), \ "Number of buckets in the interned String table") \
另外JVM還會根據hash碰撞的情況來決定是否做rehash,比如你從這個StringTable裏查找某個字符串是否存在,如果對其對應的桶挨個遍曆,超過了100個還是沒有找到對應的同名的項,那就會設置一個flag,讓下次進入到safepoint的時候做一次rehash動作,盡量減少碰撞的發生,但是當惡化到一定程度的時候,其實也沒啥辦法啦,因為你的數據量實在太大,桶子數就那麼多,那每個桶再怎麼均勻也會帶著一個很長的鏈表,所以此時我們通過修改上麵的StringTableSize將桶數變大,可能會一定程度上緩解,但是如果是java代碼的問題導致泄露,那就隻能定位到具體的代碼進行改造了。
StringTable為什麼會影響YGC
YGC的過程我不打算再這篇文章裏細說,因為我希望盡量保持每篇文章的內容不過於臃腫,有機會可以單獨寫篇文章來介紹,我這裏將列出ygc過程裏StringTable這塊的具體代碼
if (!_process_strong_tasks->is_task_claimed(SH_PS_StringTable_oops_do)) { if (so & SO_Strings || (!collecting_perm_gen && !JavaObjectsInPerm)) {
StringTable::oops_do(roots);
} if (JavaObjectsInPerm) { // Verify the string table contents are in the perm gen
NOT_PRODUCT(StringTable::oops_do(&assert_is_perm_closure));
}
}
因為YGC過程不涉及到對perm做回收,因此collecting_perm_gen
是false,而JavaObjectsInPerm
默認情況下也是false,表示String.intern返回的字符串是不是在perm裏分配,如果是false,表示是在heap裏分配的,因此StringTable指向的字符串是在heap裏分配的,所以ygc過程需要對StringTable做掃描,以保證處於新生代的String代碼不會被回收掉
至此大家應該明白了為什麼YGC過程會對StringTable掃描
有了這一層意思之後,YGC的時間長短和掃描StringTable有關也可以理解了,設想一下如果StringTable非常龐大,那是不是意味著YGC過程掃描的時間也會變長呢
YGC過程掃描StringTable對CPU影響大嗎
這個問題其實是我寫這文章的時候突然問自己的一個問題,於是稍微想了下來跟大家解釋下,因為大家也可能會問這麼個問題
要回答這個問題我首先得問你們的機器到底有多少個核,如果核數很多的話,其實影響不是很大,因為這個掃描的過程是單個GC線程來做的,所以最多消耗一個核,因此看起來對於核數很多的情況,基本不算什麼
StringTable什麼時候清理
YGC過程不會對StringTable做清理,這也就是我們demo裏的情況會讓Stringtable越來越大,因為到目前為止還隻看到YGC過程,但是在Full GC或者CMS GC過程會對StringTable做清理,具體驗證很簡單,執行下jmap -histo:live <pid>
,你將會發現YGC的時候又降下去了
最後更新:2017-04-11 19:32:01