本文将从源码角度分析String.intern方法的作用及其适用场景。OpenJDK版本
➜ jdk hg id 76072a077ee1 jdk-11 28
首先,我们来看下该方法的Javadoc文档
代码语言:javascript复制/**
* Returns a canonical representation for the string object.
* <p>
* A pool of strings, initially empty, is maintained privately by the
* class {@code String}.
* <p>
* When the intern method is invoked, if the pool already contains a
* string equal to this {@code String} object as determined by
* the {@link #equals(Object)} method, then the string from the pool is
* returned. Otherwise, this {@code String} object is added to the
* pool and a reference to this {@code String} object is returned.
* <p>
* It follows that for any two strings {@code s} and {@code t},
* {@code s.intern() == t.intern()} is {@code true}
* if and only if {@code s.equals(t)} is {@code true}.
* <p>
* All literal strings and string-valued constant expressions are
* interned. String literals are defined in section 3.10.5 of the
* <cite>The Java™ Language Specification</cite>.
*
* @return a string that has the same contents as this string, but is
* guaranteed to be from a pool of unique strings.
* @jls 3.10.5 String Literals
*/
public native String intern();
其实上面文档说的还是比较清楚的,当该方法被调用时,如果JVM内部维护的string pool中已经存在和这个string内容相同的string,就返回pool中的string,否则的话,就会先把这个string放入pool中,然后再返回这个string。
不过,为了加深对该方法的理解,我们还是从源码角度再看下。
由上可知,intern是个native方法,所以我们要先找到这个方法对应的C代码
C文件src/java.base/share/native/libjava/String.c
代码语言:javascript复制JNIEXPORT jobject JNICALL
Java_java_lang_String_intern(JNIEnv *env, jobject this)
{
return JVM_InternString(env, this);
}
该方法调用了JVM_InternString,看下这个方法
C 文件src/hotspot/share/prims/jvm.cpp
代码语言:javascript复制JVM_ENTRY(jstring, JVM_InternString(JNIEnv *env, jstring str))
...
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方法
C 文件src/hotspot/share/classfile/stringTable.cpp
代码语言:javascript复制oop StringTable::intern(oop string, TRAPS) {
if (string == NULL) return NULL;
...
Handle h_string (THREAD, string);
jchar* chars = java_lang_String::as_unicode_string(string, length,
CHECK_NULL);
oop result = intern(h_string, chars, length, CHECK_NULL);
return result;
}
...
oop StringTable::intern(Handle string_or_null_h, jchar* name, int len, TRAPS) {
...
unsigned int hash = java_lang_String::hash_code(name, len);
...
return StringTable::the_table()->do_intern(string_or_null_h, name, len,
hash, CHECK_NULL);
}
继续看下StringTable::the_table()->do_intern方法
C 文件src/hotspot/share/classfile/stringTable.cpp
代码语言:javascript复制oop StringTable::do_intern(Handle string_or_null_h, jchar* name,
int len, uintx hash, TRAPS) {
...
StringTableLookupOop lookup(THREAD, hash, string_h);
StringTableCreateEntry stc(THREAD, string_h);
...
_local_table->get_insert_lazy(THREAD, lookup, stc, stc, &rehash_warning);
...
return stc.get_return();
}
该方法中,_local_table就是JVM内部维护的string pool,其类型类似与Java中的ConcurrentHashMap。
StringTableLookupOop类是用于查询_local_table中是否存在对应的string。
StringTableCreateEntry类的作用是,当_local_table中不存在对应string时,用它来创建一个存放着新string的WeakHandle,之后这个WeakHandle会被添加到_local_table中。
StringTableCreateEntry类还有另外一个作用,就是用于接收最终的结果string,不管这个string是新创建的,还是原来就存在的。这也是为什么get_insert_lazy方法第四个参数还是stc的原因。
最后,StringTable::do_intern方法调用stc.get_return()返回结果,即,如果有对应的string,则返回对应的string,如果没有,则返回原string。
我们再来看下StringTableLookupOop和StringTableCreateEntry这两个类的实现
C 文件src/hotspot/share/classfile/stringTable.cpp
代码语言:javascript复制class StringTableLookupOop : public StackObj {
private:
Thread* _thread;
uintx _hash;
Handle _find;
...
public:
StringTableLookupOop(Thread* thread, uintx hash, Handle handle)
: _thread(thread), _hash(hash), _find(handle) { }
...
bool equals(WeakHandle<vm_string_table_data>* value, bool* is_dead) {
oop val_oop = value->peek();
...
bool equals = java_lang_String::equals(_find(), val_oop);
if (!equals) {
return false;
}
...
return true;
}
};
上面equals方法就是用来检测_local_table中的string是否就是我们想要的string。
代码语言:javascript复制class StringTableCreateEntry : public StackObj {
private:
Thread* _thread;
Handle _return;
Handle _store;
public:
StringTableCreateEntry(Thread* thread, Handle store)
: _thread(thread), _store(store) {}
WeakHandle<vm_string_table_data> operator()() { // No dups found
WeakHandle<vm_string_table_data> wh =
WeakHandle<vm_string_table_data>::create(_store);
return wh;
}
void operator()(bool inserted, WeakHandle<vm_string_table_data>* val) {
oop result = val->resolve();
assert(result != NULL, "Result should be reachable");
_return = Handle(_thread, result);
}
oop get_return() const {
return _return();
}
};
上面的第一个operator()方法就是用来创建_local_table中的WeakHandle,来存放新的string。第二个operator()方法就是用来接收最终结果,其中inserted参数用来表示这个string是否是新添加的。
至此,源码分析就结束了。
结合上面的Javadoc文档和源码分析,我们来想下,String.intern方法的适用场景究竟是什么呢?
首先,从Javadoc文档中我们可以得知,literal string本身就已经被intern了,所以intern方法对动态创建的string才有意义。
其次,从源码中我们可以看到,intern方法的逻辑还是比较复杂的,所以对于它返回的结果,我们不应该是立即使用,然后就丢弃,这样得不偿失。
最后,由intern本身的机制我们可以得知,调用intern方法的string必须是大量重复的,否则也没有意义。
对于这些限制条件,我第一想到的适用场景是,从网络接收的string,且这些string是提前预设的ID,且这些string会派发到其他线程做后续处理。这个场景也基本满足了动态创建、延迟使用、大量重复等特性。不过之后我做了些性能测试,发现intern的性能比我预想的要差很多,所以这种场景也不太适用了。
最后总结下,intern方法主要用于那些,动态创建的,会较长时间存活的,最好是会多次使用的,且存在大量重复的string。并且,调用intern方法的代码段对性能没有非常严格的要求。