概述
之所以想写这篇文章,是因为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的时候又降下去了