1. 程式人生 > >深入理解String#intern

深入理解String#intern

引言

在 JAVA 語言中有8中基本型別和一種比較特殊的型別String。這些型別為了使他們在執行過程中速度更快,更節省記憶體,都提供了一種常量池的概念。常量池就類似一個JAVA系統級別提供的快取。

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

  • 直接使用雙引號宣告出來的String物件會直接儲存在常量池中。
  • 如果不是用雙引號宣告的String物件,可以使用String提供的intern方法。intern 方法會從字串常量池中查詢當前字串是否存在,若不存在就會將當前字串放入常量池中

字串常量池(String Pool)儲存著所有字串字面量(literal strings),這些字面量在編譯時期就確定。不僅如此,還可以使用 String 的 intern() 方法在執行過程中將字串新增到 String Pool 中。

當一個字串呼叫 intern() 方法時,如果 String Pool 中已經存在一個字串和該字串值相等(使用 equals() 方法進行確定),那麼就會返回 String Pool 中字串的引用;否則,就會在 String Pool 中新增一個新的字串,並返回這個新字串的引用。

intern 的實現原理

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

intern() 方法需要傳入一個字串物件(已存在於堆上),然後檢查 StringTable 裡是不是已經有一個相同的拷貝。StringTable 可以看作是一個 HashSet,它將字串分配在永久代上。StringTable 存在的唯一目的就是維護所有存活的字串的一個物件。如果在 StringTable 裡找到了能夠找到所傳入的字串物件,那就直接返回它,否則,把它加入 StringTable 。


JAVA 使用 jni 呼叫c++實現的StringTableintern方法,  StringTableintern方法跟Java中的HashSet的實現是差不多的, 只是不能自動擴容。預設大小是1009。

要注意的是,String的String Pool是一個固定大小的Hashtable,預設值大小長度是1009,如果放進String Pool的String非常多,就會造成Hash衝突嚴重,從而導致連結串列會很長,而連結串列長了後直接會造成的影響就是當呼叫String.intern時效能會大幅下降(因為要一個一個找)。

在 jdk6中StringTable是固定的,就是1009的長度,所以如果常量池中的字串過多就會導致效率下降很快。在jdk7中,StringTable

的長度可以通過一個引數指定:

  • -XX:StringTableSize=99991

jdk6 和 jdk7 下 intern 的區別

來看一段程式碼:

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
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中的解釋

jdk6圖

注:圖中綠色線條代表 string 物件的內容指向。 黑色線條代表地址指向。

如上圖所示。在 jdk6中上述的所有列印都是 false 的,jdk6中的常量池是放在 Perm 區中的,Perm 區和正常的 JAVA Heap 區域是完全分開的。 JAVA Heap 區域的物件地址和字串常量池的物件地址是不相同的,即使呼叫String.intern方法也是沒有任何關係的。

####2,jdk7中的解釋

在 jdk6 以及以前的版本中,字串的常量池是放在堆的 Perm 區的,Perm 區是一個類靜態的區域,主要儲存一些載入類的資訊,常量池,方法片段等內容,預設大小隻有4m,一旦常量池中大量使用 intern 是會直接產生java.lang.OutOfMemoryError: PermGen space錯誤的。 到了jdk7 的版本,字串常量池已經從 Perm 區移到正常的 Java Heap 區域了。移除永久代的工作從JDK1.7就開始了,JDK1.7中,儲存在永久代的部分資料就已經轉移到了Java Heap或者是 Native Heap。但永久代仍存在於JDK1.7中,並沒完全移除,符號引用(Symbols)轉移到了native heap;字面量(interned strings)轉移到了java heap;類的靜態變數(class statics)轉移到了java heap。在 JDK 1.8 中,HotSpot 已經沒有 “PermGen space”這個區間了,取而代之是一個叫做 Metaspace(元空間) 的東西。元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機器中,而是使用本地記憶體。因此,預設情況下,元空間的大小僅受本地記憶體限制,但可以通過以下引數來指定元空間的大小:

正是因為字串常量池移動到 JAVA Heap 區域後,再來解釋為什麼會有上述的列印結果。

jdk7圖1

  • 在第一段程式碼中,先看 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 的引用地址明顯不同。圖中畫的很清晰。

jdk7圖2

  • 來看第二段程式碼,從上邊第二幅圖中觀察。第一段程式碼和第二段程式碼的改變就是 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 方法時,如果存在堆中的物件,會直接儲存物件的引用,而不會重新建立物件。

總結

通過字面量賦值建立字串時,會優先在常量池中查詢是否已經存在相同的字串,倘若已經存在,棧中的引用直接指向該字串;倘若不存在,則在常量池中生成一個字串,再將棧中的引用指向該字串。而通過new的方式建立字串時,就直接在堆中生成一個字串的物件(備註,JDK 7 以後,HotSpot 已將常量池從永久代轉移到了堆中)棧中的引用指向該物件。對於堆中的字串物件,可以通過 intern() 方法來將字串新增的常量池中,並返回指向該常量的引用。