閱讀469 返回首頁    go 微軟 go windows


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,於是我們得到的輸出結果類似下麵的

640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=
有沒有發現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

  上一篇:go JVM源碼分析之不保證順序的Class.getMethods
  下一篇:go 假笨說-謹防JDK8重複類定義造成的內存泄漏