String的Intern方法

引言

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

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

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

. intern 的實現原理

1.JAVA 代碼

. intern 的實現原理

1.JAVA 代碼

public native String intern();

String#intern方法中看到,這個方法是一個 native 的方法,但註釋寫的非常明瞭。如果常量池中存在當前字符串, 就會直接返回當前字符串. 如果常量池中沒有此字符串, 會將此字符串放入常量池中後, 再返回

2native 代碼

  在 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。要注意的是,StringString Pool是一個固定大小的Hashtable,默認值大小長度是1009,如果放進String PoolString非常多,就會造成Hash衝突嚴重,從而導致鏈表會很長,而鏈表長了後直接會造成的影響就是當調用String.intern時性能會大幅下降。在 jdk6StringTable是固定的,就是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中的解釋

  在 Jdk6 以及以前的版本中,字符串的常量池是放在堆的Perm區的,Perm區是一個類靜態的區域,主要存儲一些加載類的信息,常量池,方法片段等內容,默認大小隻有4m,一旦常量池中大量使用 intern 是會直接產生java.lang.OutOfMemoryError:PermGen space錯誤的。在 jdk7 的版本中,字符串常量池已經從Perm區移到正常的Java Heap區域了。爲什麼要移動,Perm 區域太小是一個主要原因,當然據消息稱jdk8已經直接取消了Perm區域,而新建立了一個元區域。應該是jdk開發者認爲Perm區域已經不適合現在 JAVA 的發展了。正式因爲字符串常量池移動到JAVA Heap區域後,再來解釋爲什麼會有上述的打印結果。

· 在第一段代碼中,先看 s3s4字符串。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

· 再看ss2 對象。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 方法時,如果存在堆中的對象,會直接保存對象的引用,而不會重新創建對象。

.使用 intern

1.intern 正確使用例子

  接下來我們來看一下一個比較常見的使用String#intern方法的例子。


static final int MAX = 1000 * 10000;
static final String[] arr = new String[MAX];
 
public static void main(String[] args) throws Exception {
    Integer[] DB_DATA = new Integer[10];
    Random random = new Random(10 * 10000);
    for (int i = 0; i < DB_DATA.length; i++) {
        DB_DATA[i] = random.nextInt();
    }
    long t = System.currentTimeMillis();
    for (int i = 0; i < MAX; i++) {
        //arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length]));
         arr[i] = new String(String.valueOf(DB_DATA[i % DB_DATA.length])).intern();
    }
 
    System.out.println((System.currentTimeMillis() - t) + "ms");
    System.gc();
}

 運行的參數是:-Xmx2g -Xms2g -Xmn1500M 上述代碼是一個演示代碼,其中有兩條語句不一樣,一條是使用 intern,一條是未使用 intern。結果如下圖

2160ms


826ms


通過上述結果,我們發現不使用 intern 的代碼生成了1000w 個字符串,佔用了大約640m 空間。 使用了 intern 的代碼生成了1345個字符串,佔用總空間 133k 左右。其實通過觀察程序中只是用到了10個字符串,所以準確計算後應該是正好相差100w 倍。雖然例子有些極端,但確實能準確反應出 intern 使用後產生的巨大空間節省。

細心的同學會發現使用了 intern 方法後時間上有了一些增長。這是因爲程序中每次都是用了 new String 後, 然後又進行 intern 操作的耗時時間,這一點如果在內存空間充足的情況下確實是無法避免的,但我們平時使用時,內存空間肯定不是無限大的,不使用 intern 佔用空間導致 jvm 垃圾回收的時間是要遠遠大於這點時間的。 畢竟這裏使用了1000wintern 纔多出來1秒鐘多的時間。

.總結

  本文大體的描述了 String#intern和字符串常量池的日常使用,jdk 版本的變化和String#intern方法的區別,以及不恰當使用導致的危險等內容,讓大家對系統級別的 String#intern有一個比較深入的認識。讓我們在使用和接觸它的時候能避免出現一些 bug,增強系統的健壯性。

轉載博主個人總結:

jdk.16intern()方法會把首次遇到的字符串實例複製到永久代,防的也是永久代中對應字符串實例的引用,故s!=s2;s3!=s4;s.intern()=s2;s3.intern()==s4;

jdk.17intern()方法不會再複製實例到永久代,只是在常量池中記錄首次出現的實例引用(記錄的是引用,並且還得是首次出現的實例;非首次出現的實例返回的還是永久代對應字符串的引用而不是堆中對應字符串對象的引用);

ps:實際開發使用的時候,是爲了多次同樣的string公用一個內存地址,jdk1.6公用的是常量池的內存地址,jdk1.7公用的有可能是常量池的內存地址,也有可能是堆中的地址

轉自:https://blog.csdn.net/hupoling/article/details/62423613

 







發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章