引言

String。这些类型为了使他们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念。常量池就类似一个JAVA系统级别提供的缓存。String类型的常量池比较特殊。它的主要使用方法有两种:String

  • 对象会直接存储在常量池中。

String

  • 对象,可以使用String提供的intern方法。intern 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中

String#intern方法。


一, intern 的实现原理

首先深入看一下它的实现原理。



1,JAVA 代码


/**   * Returns a canonical representation for the string object.   * <p>   * A pool of strings, initially empty, is maintained privately by the   * class <code>String</code>.   * <p>   * When the intern method is invoked, if the pool already contains a   * string equal to this <code>String</code> object as determined by   * the {@link #equals(Object)} method, then the string from the pool is   * returned. Otherwise, this <code>String</code> object is added to the   * pool and a reference to this <code>String</code> object is returned.   * <p>   * It follows that for any two strings <code>s</code> and <code>t</code>,   * <code>s.intern()&nbsp;==&nbsp;t.intern()</code> is <code>true</code>   * if and only if <code>s.equals(t)</code> is <code>true</code>.   * <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();


String#intern方法中看到,这个方法是一个 native 的方法,但注释写的非常明了。“如果常量池中存在当前字符串, 就会直接返回当前字符串. 如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回”。


2,native 代码

在 jdk7后,oracle 接管了 JAVA 的源码后就不对外开放了,根据 jdk 的主要开发人员声明 openJdk7 和 jdk7 使用的是同一分主代码,只是分支代码会有些许的变动。所以可以直接跟踪 openJdk7 的源码来探究 intern 的实现。

####native实现代码:
 \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;  
}

它的大体实现结构就是:

JAVA 使用 jni 调用c++实现的StringTableintern方法, StringTableintern方法跟Java中的HashMap的实现是差不多的, 只是不能自动扩容。默认大小是1009。Hashtable,默认值大小长度是1009,如果放进String Pool的String非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用String.intern时性能会大幅下降(因为要一个一个找)。StringTable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。在jdk7中,StringTable的长度可以通过一个参数指定:-XX:StringTableSize=99991


二,jdk6 和 jdk7 下 intern 的区别

String s = new String("abc")这个语句创建了几个对象的题目。 这种题目主要就是为了考察程序员对字符串对象的常量池掌握与否。上述的语句中是创建了2个对象,第一个对象是"abc"字符串存储在常量池中,第二个对象在JAVA Heap中的 String 对象。

来看一段代码:


public static void main(String[] args) {
    String s = new String("1");
    s.intern();
    String s2 = "1";
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    s3.intern();
    String s4 = "11";
    System.out.println(s3 == s4);
}


打印结果是

false falsefalse trues3.intern();语句下调一行,放到String s4 = "11";后面。将s.intern(); 放到String s2 = "1";后面。是什么结果呢

public static void main(String[] args) {
    String s = new String("1");
    String s2 = "1";
    s.intern();
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    String s4 = "11";
    s3.intern();
    System.out.println(s3 == s4);
}


打印结果为:

false falsefalse false

####1,jdk6中的解释

java string 什么情况放常量池 string常量池原理_字符串

注:图中绿色线条代表 string 对象的内容指向。 黑色线条代表地址指向。

String.intern方法也是没有任何关系的。

####2,jdk7中的解释

java.lang.OutOfMemoryError: PermGen space错误的。 所以在 jdk7 的版本中,字符串常量池已经从 Perm 区移到正常的 Java Heap 区域了。为什么要移动,Perm 区域太小是一个主要原因,当然据消息称 jdk8 已经直接取消了 Perm 区域,而新建立了一个元区域。应该是 jdk 开发者认为 Perm 区域已经不适合现在 JAVA 的发展了。

正式因为字符串常量池移动到 JAVA Heap 区域后,再来解释为什么会有上述的打印结果。

String s3 = new String("1") + new String("1");

  • ,这句代码中现在生成了2最终个对象,是字符串常量池中的“1” 和 JAVA Heap 中的 s3引用指向的对象。中间还有2个匿名的

new String("1")

  • 我们不去讨论它们。此时s3引用对象内容是"11",但此时常量池中是没有 “11”对象的。

s3.intern();

  • 这一句代码,是将 s3中的“11”字符串放入 String 常量池中,因为此时常量池中不存在“11”字符串,因此常规做法是跟 jdk6 图中表示的那样,在常量池中生成一个 "11" 的对象,关键点是 jdk7 中常量池不在 Perm 区域了,这块做了调整。常量池中不需要再存储一份对象了,可以直接存储堆中的引用。这份引用指向 s3 引用的对象。 也就是说引用地址是相同的。

String s4 = "11";

  •  这句代码中"11"是显示声明的,因此会直接去常量池中创建,创建的时候发现已经有这个对象了,此时也就是指向 s3 引用对象的一个引用。所以 s4 引用就指向和 s3 一样了。因此最后的比较 

s3 == s4String s = new String("1");

  •  第一句代码,生成了2个对象。常量池中的“1” 和 JAVA Heap 中的字符串对象。

s.intern();String s2 = "1";

s3.intern();

  •  的顺序是放在

String s4 = "11";

  • 后了。这样,首先执行

String s4 = "11";

  • 声明 s4 的时候常量池中是不存在“11”对象的,执行完毕后,“11“对象是 s4 声明产生的新对象。然后再执行

s3.intern();

  • 时,常量池中“11”对象已经存在了,因此 s3 和 s4 的引用是不同的。

s.intern();

  • ,这一句往后放也不会有什么影响了,因为对象池中在执行第一句代码

String s = new String("1");

  • 的时候已经生成“1”对象了。下边的s2声明都是直接从常量池中取地址引用的。 s 和 s2 的引用地址是不会相等的。

####小结
从上述的例子代码可以看出 jdk7 版本对 intern 操作和常量池都做了一定的修改。主要包括2点:

  • 将String常量池 从 Perm 区移动到了 Java Heap区

String#intern