String常量池詳解

引言

在 JAVA 語言中有8中基本類型和一種比較特殊的類型String。這些類型爲了使他們在運行過程當中速度更快,更節省內存,都提供了一種常量池的概念。常量池就相似一個JAVA系統級別提供的緩存。java

8種基本類型的常量池都是系統協調的,String類型的常量池比較特殊。它的主要使用方法有兩種:c++

  • 直接使用雙引號聲明出來的String對象會直接存儲在常量池中。程序員

  • 若是不是用雙引號聲明的String對象,可使用String提供的intern方法。intern 方法會從字符串常量池中查詢當前字符串是否存在,若不存在就會將當前字符串放入常量池中express

接下來咱們主要來談一下String#intern方法。緩存

一, intern 的實現原理

首先深刻看一下它的實現原理。oracle

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 的方法,但註釋寫的很是明瞭。「若是常量池中存在當前字符串, 就會直接返回當前字符串. 若是常量池中沒有此字符串, 會將此字符串放入常量池中後, 再返回」。app

2,native 代碼

在 jdk7後,oracle 接管了 JAVA 的源碼後就不對外開放了,根據 jdk 的主要開發人員聲明 openJdk7 和 jdk7 使用的是同一分主代碼,只是分支代碼會有些許的變更。因此能夠直接跟蹤 openJdk7 的源碼來探究 intern 的實現。jvm

####native實現代碼:
\openjdk7\jdk\src\share\native\java\lang\String.coop

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。

要注意的是,String的String Pool是一個固定大小的Hashtable,默認值大小長度是1009,若是放進String Pool的String很是多,就會形成Hash衝突嚴重,從而致使鏈表會很長,而鏈表長了後直接會形成的影響就是當調用String.intern時性能會大幅降低(由於要一個一個找)。

在 jdk6中StringTable是固定的,就是1009的長度,因此若是常量池中的字符串過多就會致使效率降低很快。在jdk7中,StringTable的長度能夠經過一個參數指定:

  • -XX:StringTableSize=99991

二,jdk6 和 jdk7 下 intern 的區別

相信不少 JAVA 程序員都作作相似 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);
}

打印結果是

  • jdk6 下false false

  • jdk7 下false true

具體爲何稍後再解釋,而後將s3.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);
}

打印結果爲:

  • jdk6 下false false

  • jdk7 下false false

####1,jdk6中的解釋

注:圖中綠色線條表明 string 對象的內容指向。 黑色線條表明地址指向。

如上圖所示。首先說一下 jdk6中的狀況,在 jdk6中上述的全部打印都是 false 的,由於 jdk6中的常量池是放在 Perm 區中的,Perm 區和正常的 JAVA Heap 區域是徹底分開的。上面說過若是是使用引號聲明的字符串都是會直接在字符串常量池中生成,而 new 出來的 String 對象是放在 JAVA Heap 區域。因此拿一個 JAVA Heap 區域的對象地址和字符串常量池的對象地址進行比較確定是不相同的,即便調用String.intern方法也是沒有任何關係的。

####2,jdk7中的解釋

再說說 jdk7 中的狀況。這裏要明確一點的是,在 Jdk6 以及之前的版本中,字符串的常量池是放在堆的 Perm 區的,Perm 區是一個類靜態的區域,主要存儲一些加載類的信息,常量池,方法片斷等內容,默認大小隻有4m,一旦常量池中大量使用 intern 是會直接產生java.lang.OutOfMemoryError: PermGen space錯誤的。 因此在 jdk7 的版本中,字符串常量池已經從 Perm 區移到正常的 Java Heap 區域了。爲何要移動,Perm 區域過小是一個主要緣由,固然據消息稱 jdk8 已經直接取消了 Perm 區域,而新創建了一個元區域。應該是 jdk 開發者認爲 Perm 區域已經不適合如今 JAVA 的發展了。

正式由於字符串常量池移動到 JAVA Heap 區域後,再來解釋爲何會有上述的打印結果。

  • 在第一段代碼中,先看 s3和s4字符串。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 == s4 是 true。

  • 再看 s 和 s2 對象。 String s = new String("1"); 第一句代碼,生成了2個對象。常量池中的「1」 和 JAVA Heap 中的字符串對象。s.intern(); 這一句是 s 對象去常量池中尋找後發現 「1」 已經在常量池裏了。

  • 接下來String s2 = "1"; 這句代碼是生成一個 s2的引用指向常量池中的「1」對象。 結果就是 s 和 s2 的引用地址明顯不一樣。圖中畫的很清晰。

  • 來看第二段代碼,從上邊第二幅圖中觀察。第一段代碼和第二段代碼的改變就是 s3.intern(); 的順序是放在String s4 = "11";後了。這樣,首先執行String s4 = "11";聲明 s4 的時候常量池中是不存在「11」對象的,執行完畢後,「11「對象是 s4 聲明產生的新對象。而後再執行s3.intern();時,常量池中「11」對象已經存在了,所以 s3 和 s4 的引用是不一樣的。

  • 第二段代碼中的 s 和 s2 代碼中,s.intern();,這一句日後放也不會有什麼影響了,由於對象池中在執行第一句代碼String s = new String("1");的時候已經生成「1」對象了。下邊的s2聲明都是直接從常量池中取地址引用的。 s 和 s2 的引用地址是不會相等的。

####小結
從上述的例子代碼能夠看出 jdk7 版本對 intern 操做和常量池都作了必定的修改。主要包括2點:

  • 將String常量池 從 Perm 區移動到了 Java Heap區

  • String#intern 方法時,若是存在堆中的對象,會直接保存對象的引用,而不會從新建立對象。

相關文章
相關標籤/搜索