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内核”等,可能有一款适合你的胃口。
欢迎关注: