1. 程式人生 > 實用技巧 >JVM中字串的祕密

JVM中字串的祕密

簡介

字元陣列的儲存方式

字串常量池

字串在java程式中被大量使用,為了避免每次都建立相同的字串物件及記憶體分配,JVM內部對字串物件的建立做了一定的優化,在Permanent Generation中專門有一塊區域用來儲存字串常量池(一組指標指向Heap中的String物件的記憶體地址)。

在HotSpot VM裡實現的string pool功能的是一個StringTable類,它是一個HashTable,預設值大小長度是1009;這個StringTable在每個HotSpot VM的例項只有一份,被所有的類共享。字串常量由一個一個字元組成,放在了StringTable上。在JDK6.0中,StringTable的長度是固定的,長度就是1009,因此如果放入String Pool中的String非常多,就會造成hash衝突,導致連結串列過長,當呼叫String#intern()時會需要到連結串列上一個一個找,從而導致效能大幅度下降;

  • 在JDK6.0及之前版本中,String Pool裡放的都是字串常量;
  • 在JDK7.0中,由於String#intern()發生了改變,因此String Pool中也可以存放放於堆內的字串物件的引用。關於String在記憶體中的儲存和String#intern()方法的說明。

字串Hashcode

不通方式建立字串在JVM儲存的形式

  • 雙引號方式

雙引號引起來的字串,首先從常量池中查詢是否存在此字串。如果不存在,則在常量池中新增此字串。在堆中建立字串物件,因String底層是通過char陣列形式儲存的,所以同時會在堆中生成一個TypeArrayOopDesc用來儲存char陣列物件。如果存在,則直接引用此字串物件。

測試程式碼1: 

public static  void test1(){
        String s1="11";
        String s2="11";

        System.out.println(s1==s2);
    }

測試結果:

原因分析:

s1程式碼執行後,常量池中添加了“11”這個常量,在堆中也建立了String物件並引用此常量的。當s2程式碼執行時,先在常量池中查詢是否存在“11”這個常量,發現常量池中存在這個值,就找到引用此常量的字串物件,將s2的引用指向找到的字串物件。因為s1和s2指向同一個地址,所以比較結果為true。   

  • new String

1、首先從常量池中查詢是否存在括號內的常量,如果不存在,則在常量池中新增此字串。在堆中建立字串物件,因String底層是通過char陣列形式儲存的,所以同時會在堆中生成一個TypeArrayOopDesc用來儲存char陣列物件。如果存在,則直接引用堆中存在的字串物件。

2、通過new方式建立的String物件,每次都會在Heap上建立一個新的例項。並將此新例項中char陣列物件,指向第一步堆中的已經存在的TypeArrayOopDesc。

測試程式碼:

public static void test2() {
        String s1 = new String("11");
        String s2 = new String("11");

        System.out.println(s1 == s2);
    }

測試結果:

原因分析:

通過new方式建立的String物件,每次都會在Heap上建立一個新的例項。所以s1和s2的分別指向了不同的例項,引用地址不同。

測試程式碼:

 public static void test3() {
        String s1 = new String("11");
        String s2 = "11";

        System.out.println(s1 == s2);
    }

測試結果:

原因分析:

當執行s1時,首先會將括號內的字面量常量“11”新增到常量池中,並且在堆中生成字串例項及char陣列例項TypeArrayOopDesc。再通過new方式建立的String物件,會在Heap上新建立一個例項,此新例項中char陣列不需要新的例項,指向堆中的已存在的TypeArrayOopDesc。

當執行s2時,在常量池中發現常量已存在,則直接將虛擬機器棧的指向堆中代表此常量的字串例項。

因此s1和s2的分別指向了不同的例項,引用地址不同。

【缺圖】

字串在JVM中是如何拼接的

測試程式碼:

 public static void test4(){
        String s2="1"+"1";
        String s1="11";


        System.out.println(s1==s2);
    }

測試結果:

原因分析:

檔案在編譯期成位元組碼時,編譯器將“1”+“1”變成了“11”,編譯後,相當於s2="11"。就與上面的測試程式碼1相同了,具體原因見測試程式碼1的原因分析。

測試程式碼:

  public static void test5(){
        String s1="1";
        String s2="1";
        String s3=s1+s2;
        String s4="11";

        System.out.println(s3==s4);
    }

測試結果:

原因分析:

編譯器在編譯時無法確定s3的值,是在執行時才能確定,儲存在jvm的堆裡面,在拼接的時候,先在常量池裡面生成是s1、s2的字串,在執行加號的時候,會從常量池中取出s1、s2常量,在堆中生成兩個字串物件,然後再生成第三個字串物件來儲存兩個物件拼接後的值。

測試程式碼:

 public static void test6() {
        final String s1 = "1";
        final String s2 = "1";
        String s3 = s1 + s2;
        String s4 = "11";

        System.out.println(s3 == s4);
    }

測試結果:

原因分析:

通過s1、s2增加final修飾符,s1和s2的值賦值後不允許改變,這樣編譯器在編譯時會把s3編譯成s3="11",所以在執行時會字串常量池中新增“11”這個常量,執行s4時會在常量池中找到“11”這個常量, s4會執行堆中已存在的字串物件。因此s3和s4相等。

intern做了什麼

intern()方法:

public String intern()

JDK原始碼如下圖:

返回字串物件的規範化表示形式。

一個初始時為空的字串池,它由類 String 私有地維護。

當呼叫 intern 方法時,如果池已經包含一個等於此 String 物件的字串(該物件由 equals(Object) 方法確定),

則返回池中的字串。否則,將此 String 物件新增到池中,並且返回此 String 物件的引用。

它遵循對於任何兩個字串 s 和 t,當且僅當 s.equals(t) 為 true 時,s.intern() == t.intern() 才為 true。

所有字面值字串和字串賦值表示式都是內部的。

返回:

一個字串,內容與此字串相同,但它保證來自字串池中。

儘管在輸出中呼叫intern方法並沒有什麼效果,但是實際上後臺這個方法會做一系列的動作和操作。

在呼叫”ab”.intern()方法的時候會返回”ab”,但是這個方法會首先檢查字串池中是否有”ab”這個字串,

如果存在則返回這個字串的引用,否則就將這個字串新增到字串池中,然會返回這個字串的引用。

測試程式碼:

public static void test8_3(){
        String s1="11";
        String s2=new String("11");
        String s3=s2.intern();

        System.out.println(s1==s2);//#1
        System.out.println(s1==s3);//#2
    }

測試結果:

原因分析:

結果 #1:因為s1指向的是字串中的常量,s2是在堆中生成的物件,所以s1==s2返回false。

結果 #2:s2呼叫intern方法,會將s2中值(“string”)複製到常量池中,但是常量池中已經存在該字串(即s1指向的字串),

所以直接返回該字串的引用,因此s1==s2返回true。