HotSpot中使用StringTable来缓存字符串常量,以提高程序的运行性能。在Java语言中String类被final关键字修饰,意味着String类不能被继承,并且它的成员方法都默认为final方法;字符串一旦创建就不能再修改。String实例的值是通过字符数组实现字符串存储的。使用字符串池的优点就是避免了相同内容的字符串的创建,节省了内存,省去了创建相同字符串的时间,同时提升了性能;另一方面,字符串池的缺点就是牺牲了JVM在常量池中遍历对象所需要的时间,不过其时间成本相比而言比较低。
一.StringTable
HotSpot内部StringTable是一个Hash表
public class Neo {
public static void main(String[] args) {
String neo = "the one";
String murphys = "Master";
}
}
使用javap -v Neo.class 查看字节码使用了ldc字节码指令
0: ldc #2 // String the one
2: astore_1
3: ldc #3 // String Master
5: astore_2
6: return
在字节码解释器中ldc指令根据常量池tag类型来判断常量类型,对于JVM_CONSTANT_String类型,调用resolve_ldc方法来处理
src/share/vm/interpreter/bytecodeInterpreter.cpp
CASE(_ldc):
{
......
ConstantPool* constants = METHOD->constants();
switch (constants->tag_at(index).value()) {
......
case JVM_CONSTANT_String:
{
oop result = constants->resolved_references()->obj_at(index);
CALL_VM(InterpreterRuntime::resolve_ldc(THREAD, (Bytecodes::Code) opcode), handle_exception);
THREAD->set_vm_result(NULL);
break;
}
src/share/vm/interpreter/interpreterRuntime.cpp
IRT_ENTRY(void, InterpreterRuntime::resolve_ldc(JavaThread* thread, Bytecodes::Code bytecode)) {
ResourceMark rm(thread);
methodHandle m (thread, method(thread));
Bytecode_loadconstant ldc(m, bci(thread));
oop result = ldc.resolve_constant(CHECK);
#endif
thread->set_vm_result(result);
}
IRT_END
src/share/vm/interpreter/bytecode.cpp
oop Bytecode_loadconstant::resolve_constant(TRAPS) const {
int index = raw_index();
ConstantPool* constants = _method->constants();
if (has_cache_index()) {
return constants->resolve_cached_constant_at(index, THREAD);
} else {
return constants->resolve_constant_at(index, THREAD);
}
}
src/share/vm/oops/constantPool.hpp
oop resolve_cached_constant_at(int cache_index, TRAPS) {
constantPoolHandle h_this(THREAD, this);
return resolve_constant_at_impl(h_this, _no_index_sentinel, cache_index, THREAD);
}
src/share/vm/oops/constantPool.cpp
oop ConstantPool::resolve_constant_at_impl(const constantPoolHandle& this_cp,
int index, int cache_index, TRAPS) {
constantTag tag = this_cp->tag_at(index);
switch (tag.value()) {
case JVM_CONSTANT_String:
//
result_oop = string_at_impl(this_cp, index, cache_index, CHECK_NULL);
break;
}
}
src/share/vm/oops/constantPool.cpp
oop ConstantPool::string_at_impl(const constantPoolHandle& this_cp, int which, int obj_index, TRAPS) {
// 如果字符串已被截取,则此项将为非空
oop str = this_cp->resolved_references()->obj_at(obj_index);
if (str != NULL) return str;
//如果为空,则重新生成符号
Symbol* sym = this_cp->unresolved_string_at(which);
//没错,放到字符串常量表中
str = StringTable::intern(sym, CHECK_(NULL));
this_cp->string_at_put(which, obj_index, str);
return str;
}
intern函数会首先从StringTable中查找是否存在相同的字符串常量,存在则直接取回,不存在则重新生成一个同时放到StringTable中
src/share/vm/classfile/stringTable.cpp
oop StringTable::intern(Handle string_or_null, jchar* name,
int len, TRAPS) {
//生成hash码从共享表中找
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);
// Found
if (found_string != NULL) {
if (found_string != string_or_null()) {
ensure_string_alive(found_string);
}
return found_string;
}
Handle string;
// try to reuse the string if possible
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);
// Otherwise, add to symbol to table
added_or_found = the_table()->basic_add(index, string, name, len,
hashValue, CHECK_NULL);
}
return added_or_found;
}
对于String类中的intern方法,同样调用 StringTable::intern,intern 方法是一个native方法,intern方法会从字符串常量池中查询当前字符串是否存在,如果存在,就直接返回当前字符串;如果不存在就会将当前字符串放入常量池中,之后再返回。
java/lang/String.java
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
public native String intern();
}
src/share/vm/prims/jvm.cpp
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
二.诡异行为
在Java中,String是不可变字符串对象,StringBuilder和StringBuffer是可变字符串对象(其内部的字符数组长度可变)。String中的对象是不可变的,也就可以理解为常量,显然线程安全。StringBuffer 与 StringBuilder 中的方法和功能完全是等价的,只是StringBuffer 中的方法大都采用了synchronized 关键字进行修饰,因此是线程安全的,而 StringBuilder 没有这个修饰,可以被认为是非线程安全的。当字符串相加操作较多的情况下,建议使用StringBuilder,如果采用了多线程,则使用StringBuffer。在JDK11下String的比较表现出如下行为
public class Neo {
public static void main(String[] args) {
String s1 = "the one";
String s2 = new String("the one");
String s3 = s1.intern();
String s4 = s2.intern();
System.out.println(s1 == s2); //false
System.out.println(s1.equals(s2)); //true
System.out.println(s1.equals(s3)); //true
System.out.println(s1 == s3); //true
System.out.println(s1 == s4); //true
System.out.println(s2 == s4); //false
}
}
- 对于s1 ==s2 为false,比较的是对象,即在HotSpot内部s1,s2指向两个不同的对象。
- 对于s1.equals(s2)为true,比较的是对象的内容
- 对于s1.equals(s3)为true,比较的是对象的内容
- 对于s1 ==s3为true,s3是由s1.intern()返回的,此时"the one"已在StringTable,虚拟机返回了s1的oop
- 对于s1 ==s4为true,s4是s2.intern()返回的,此时"the one"已在StringTable,虚拟机返回了s1的oop,没错是s1
- 对于s2 ==s4为false,s4是s2.intern()返回的,此时"the one"已在StringTable,虚拟机返回了s1的oop,没错是s1
上述创建了两个对象:s1,s2,对于s3,由于字符串常量已存在月StringTable中,并且是由s1存进去的。s1.intern()返回的是对s1的引用。对于s4,虽然它是s2.intern()返回的,但StringTable中的常量是由s1存进去的,所以s2.intern()返回的仍然是s1的引用,诡异得很。所以s4 != s2。