字符串去重是G1引入的新特性,在我们日常开发中,字符串基本上是使用最多的类型。而String对象是不可变的,通常会消耗大量的内存,这里面有一部分是冗余的。String对象有自己的专属char[],而这个char没有暴露给客户端。那么jvm就可以通过判断两个字符串是否一致,从而将一个字符串的char[]共享为另一个字符串的char[],从而达到节省内存的目的。
字符串去重步骤
- 找到能去重的对象
字符串是否可以去重需要满足2个条件:1. 字符串位于新生代,若是复制到s区,则年龄>StringDeduplicationAgeThreshold(默认为3),才可以去重;若是字符串晋升到old区,且非大对象且年龄<StringDeduplicationAgeThreshold,则可以去重;2. fullgc只考虑第二个条件
bool G1StringDedup::is_candidate_from_mark(oop obj) {
if (java_lang_String::is_instance(obj)) {
bool from_young = G1CollectedHeap::heap()->heap_region_containing_raw(obj)->is_young();
if (from_young && obj->age() < StringDeduplicationAgeThreshold) {
//young to old且年龄<StringDeduplicationAgeThreshold
return true;
}
}
return false;
}
void G1StringDedup::enqueue_from_mark(oop java_string) {
assert(is_enabled(), "String deduplication not enabled");
if (is_candidate_from_mark(java_string)) {
G1StringDedupQueue::push(0 /* worker_id */, java_string);
}
}
bool G1StringDedup::is_candidate_from_evacuation(bool from_young, bool to_young, oop obj) {
if (from_young && java_lang_String::is_instance(obj)) {
if (to_young && obj->age() == StringDeduplicationAgeThreshold) {
//young to young,即到s区,且年龄>StringDeduplicationAgeThreshold
return true;
}
if (!to_young && obj->age() < StringDeduplicationAgeThreshold) {
//young to old且年龄<StringDeduplicationAgeThreshold
return true;
}
}
// Not a candidate
return false;
}
- 去重
由单独的去重线程完成。开始去重时,会先查找字符数组是否存在,若存在则调整指针,共享字符串数组,释放多余的字符串数组。
去重在G1CollectHeap初始化中启动,由hashtable存储字符串数组的值,在字符串回收时可以对hashtable进行遍历。 - 回收
在ygc、并发标记、fullgc中都有可能进行字符串去重的回收。
参数
字符串去重的参数有UseStringDeduplication和StringDeduplicationAgeThreshold,前者代表是否开启去重功能,后者如上述。
虽然字符串去重能明显减少内存,但增加了gc处理时间,使用时需验证效果。
字符串去重和String.intern方法
String.intern方法:通过StringTable(HashTable实现)存储字符串对象,如果StringTable已存在该对象,则返回该对象在常量池中的引用。否则,在StringTable加入该对象,然后返回引用。
两者区别有2点:
- intern缓存的是字符串对象,而字符串去重是字符串里的字符数组。这里intern这么搞主要是gc并发标记用。试想如果intern使用字符数组的话,那么当Str1死亡还未回收时,另一个Str2执行intern则发现在Stringtable中没有,又会加入到Stringtable中,而此时只需激活Str1并返回引用即可。这里假设Str1和Str2是equals。
- intern需要显式调用,字符串去重是jvm自己处理。