由常量池 运行时常量池 String intern方法想到的(四)之深入理解intern

时间:2022-12-29 17:22:41

上篇博文由常量池 运行时常量池 String intern方法想到的(三),写到了String的内存模型,这篇博文讨论下String#intern方法的实现。
这篇博文主要参考自:深入解析String#intern。只是用来学习,无意侵犯版权。

声明

本文的讨论如在不特殊说明的前提下,使用的是JDK1.6,在特殊场合会说明使用的是JDK1.6还是JDK1.8。
JDK1.6

java version "1.6.0_45"
Java(TM) SE Runtime Environment (build 1.6.0_45-b06)
Java HotSpot(TM) 64-Bit Server VM (build 20.45-b01, mixed mode)

JDK1.8

Microsoft Windows [版本 6.1.7601]
版权所有 (c) 2009 Microsoft Corporation。保留所有权利。

C:\Users\Administrator>java -version
java version "1.8.0_65"
Java(TM) SE Runtime Environment (build 1.8.0_65-b17)
Java HotSpot(TM) 64-Bit Server VM (build 25.65-b01, mixed mode)

结论

String会存放在运行时常量池的可能性只有下面两种可能:

  • 在java代码中所有以双引号(”“)声明出来的字符串都会放到运行时常量池中。
  • 当String对象调用intern方法时,会在运行时常量池中查找,看在运行时常量池中有无该字符串,如果有会直接返回该字符串的引用,如果没有则放入运行时常量池中。
    当常量池中没有该字符串时,JDK7及以后的intern()方法的实现不再是在常量池中创建与此String内容相同的字符串,而改为在常量池中记录Java Heap中首次出现的该字符串的引用,并返回该引用。
    使用运行时常量池的作用可以减少内存使用。因为,如果在运行时常量池中有该字符串就不再创建新的字符串。

String#intern源码

java给intern的注释如下所示:

/**
* 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&trade; 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.
*/
public native String intern();

上面的注释说的很清楚:

  • 如果运行时常量池中有这个字符串,直接返回这个字符串在运行时常量池中的地址;如果运行时常量池中没有这个字符串,则将这个字符串添加到运行时常量池中,并返回这个字符串的引用(注意没有写清楚到底是运行时常量池的地址还是字符串的原地址)。
  • 当且仅当s.equals(t)返回true时,s.intern()==t.intern()的结果才是true

上面的描述中的第一条,当运行时常量池中没有这个字符串时,JDK1.6和JDK1.7的实现是不一样的。在JDK1.6中,会将这个字符串复制一份存放在运行时常量池中,而JDK1.7之后会存放这个字符串的引用。(后面会举例说明)。

intern的实现

OpenJDK1.6和OpenJDK1.7的下载地址如下:
OpenJDK1.6下载地址
OpenJDK1.7下载地址
从上面可以看到intern方法是一个native方法,也就是会调用非java实现的代码。
\openjdk7\jdk\src\share\native\java\lang\String.c

Java_java_lang_String_intern(JNIEnv *env, jobject this) 
{
return JVM_InternString(env, this);
}

\openjdk7\hotspot\src\share\vm\prims\jvm.h

/* 
* java.lang.String
*/

JNIEXPORT jstring JNICALL
JVM_InternString(JNIEnv *env, jstring str);

\openjdk7\hotspot\src\share\vm\prims\jvm.cpp

// String support ///////////////////////////////////////////////////////////////////////////  
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

\openjdk7\hotspot\src\share\vm\classfile\symbolTable.cpp

oop StringTable::intern(Handle string_or_null, jchar* name,  
int len, TRAPS) {
unsigned int hashValue = java_lang_String::hash_string(name, len);
int index = the_table()->hash_to_index(hashValue);
oop string = the_table()->lookup(index, name, len, hashValue);
// Found
if (string != NULL) return string;
// Otherwise, add to symbol to table
return the_table()->basic_add(index, string_or_null, name, len,
hashValue, CHECK_NULL);
}

\openjdk7\hotspot\src\share\vm\classfile\symbolTable.cpp

oop StringTable::lookup(int index, jchar* name,  
int len, unsigned int hash) {
for (HashtableEntry<oop>* l = bucket(index); l != NULL; l = l->next()) {
if (l->hash() == hash) {
if (java_lang_String::equals(l->literal(), name, len)) {
return l->literal();
}
}
}
return NULL;
}

从上面的代码中可以看到intern的核心实现是用C++实现的。C++有一个哈希表,其结构如下所示:
由常量池 运行时常量池 String intern方法想到的(四)之深入理解intern
这个哈希表的长度在JDK1.6中是固定的,为1009,JDK1.7及以后可以通过参数配置:-XX:StringTableSize=9999。
上面代码的实现逻辑是这样的:根据string和len求得该字符串的hash值,然后求得对应的hash表的下标index,然后逐个遍历这个链表。在遍历链表过程中,先判断hash值是否相同,如果不同,这直接下次循环;如果相同才会判断值是否相同。
由于hash表长度的限制,如果放入运行时常量池中的字符串比较多,则造成冲突的可能性比较大,导致链表长度比较长,查询操作就比较费时。
从上面intern的实现可以看出,当JVM加载class文件,将常量池存入运行时常量池中时,会经历hash表的查找过程。

案例分析

1.首先验证JDK1.6和JDK1.8的执行结果是不同的。

public class Test {
public static void main(String[] args) {
String s = new String("12") + new String("3");
System.out.println(s == s.intern());
}
}

JDK1.6的执行结果为:false
JDK1.8的执行结果为:true
JDK1.6的内存模型如下所示:
由常量池 运行时常量池 String intern方法想到的(四)之深入理解intern
JDK1.8的内存模型如下所示:
由常量池 运行时常量池 String intern方法想到的(四)之深入理解intern
经过上一篇博文的分析,当执行String s = new String("12") + new String("3"); 时在运行时常量池中并没有字符串”123”,只有当s.intern()时才会将其放入到运行时常量池。由于JDK1.6,会在运行时常量池中拷贝一份原字符串返回该字符串在运行时常量池中的地址,s指向的是堆中的地址,因此这两个地址显然不同。
2.

        String s = new String("1");
s.intern(); //这句话其实没什么意义,因为“1”已经在运行时常量池中了,返回的是运行时常量池中的地址。
String s2 = "1";
System.out.println(s == s2); //必为false

String s3 = new String("1") + new String("1");
s3.intern(); //这句话是有作用的,因为"11"从未在运行时常量池中出现过,执行了该语句之后,运行时常量池中才有"11",但是JDK1.6和JDK1.7之后的实现方式不同。
String s4 = "11";
System.out.println(s3 == s4);

运行结果如下所示:
//JDK1.6
false false
//JDK1.8
false true
3.

        String s = new String("1");
String s2 = "1";
s.intern(); //这句话没啥用
System.out.println(s == s2); //必为false

String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern(); //这句话也没啥用
System.out.println(s3 == s4); //必为false

//JDK1.6
false false
//JDK1.8
false false
4.

        String s1=new String("xy") + "z"; 
String s2=s1.intern(); //这句JDK1.6和JDK1.8的处理方式是不同的,JDK1.6复制一份“xyz”在运行时常量池,JDK1.8是在运行时常量池中存一份s1的引用
System.out.println( s1==s1.intern() );
System.out.println( s1+" "+s2 );
System.out.println( s2==s1.intern() ); //必为true

//JDK1.6
false
xyz xyz
true
//JDK1.8
true
xyz xyz
true
5.

        String s1=new String("xyz") ; 
String s2=s1.intern();//这句话没啥用
System.out.println( s1==s1.intern() ); //必为false
System.out.println( s1+" "+s2 );
System.out.println( s2==s1.intern() ); //必为true

//JDK1.6
false
xyz xyz
true
//JDK1.8
false
xyz xyz
true
6.

        String s1 = "xy" + "z";
String s2 = s1.intern();//这句话没啥用
System.out.println( s1==s1.intern() );//必为true
System.out.println( s1+" "+s2 );
System.out.println( s2==s1.intern() ); //必为true

//JDK1.6
true
xyz xyz
true
//JDK1.8
true
xyz xyz
true

结束语

如果这几个案例有不懂的,推荐看这篇文章由常量池 运行时常量池 String intern方法想到的(三) ,内存模型清楚了,就不会做错。

参考资料:
1. 深入解析String#intern
2. String放入运行时常量池的时机与String.intern()方法解惑