天天看點

Java 幹貨之深入了解String

可以證明,字元串操作是計算機程式設計中最常見的行為,尤其是在Java大展拳腳的Web系統中更是如此。

---《Thinking in Java》

提到Java中的String,總是有說不完的知識點,它對于剛接觸Java的人來說,有太多太多的值得研究的東西了,可是為什麼Java中的String這麼獨特呢?今天我們來一探究竟。

基本資料類型

衆所周知Java有八大基本資料類型,那麼基本資料類型與對象有什麼異同呢?

  • 基本資料類型不是對象
  • 基本資料類型能直接存儲變量對應的值在堆棧中,存取更加高效
  • 使用友善,不用new建立,每次表示都不用建立一個對象

字面量與指派

什麼叫字面值呢?考慮下面代碼:

int a=3;
double d=3.32;
long l=102322332245L;
           

其中,3、3.32、102322332245L便叫做字面值。3預設是int類型,3.32預設是double類型,102322332245預設也是int類型,是以必須加一個L來将它修改為long類型,否則編譯器就會報錯,字面量可以直接在計算機中表示。

基本資料類型便可以直接通過字面值進行指派

String與基本資料類型

話說了這麼多,這和String有什麼關系呢?正如本文最開始所說,因為Java需要常常與字元串打交道,是以Java的設計者想要将String類型在使用上和性能上盡量像基本資料類型一樣。

也就是

int i=0;
String str="test";
           

那麼問題來了,基本資料類型之是以叫基本資料類型,是因為這種類型可以直接在計算機中直接被表示,比如

int i=0;

中,0作為字面值是可以直接被表示出來"0x00",而

test

作為字元串如何直接被表示呢?

常量池

JVM的解決方案便是Class檔案常量池。Class常量池中主要用于存放兩大類常量: 字面量和符号引用量,其中字面量便包括了文本字元串。

也就是當我們在類中寫下

String str="test"

的時候,Class檔案中,就會出現

test

這個字面量。

String

類型的特殊性在于它隻需要一個utf8表示的内容即可。

這樣便解決了

String

直接指派的問題,隻要在JVM中,将str與

test

字面量對應起來即可。

也就是類似

int a=0; //a 的值為數值0
String str="test" //str内容為常量池中的utf8 test 

           

但是,問題就真的這麼簡單麼?

可别忘了,

String

也是一個對象,它也同時擁有所有一個對象應該擁有的特點,也就說

String str="test"
           

其中

test

字面量不僅僅需要表示

str

指向的内容是

test

,它還應該将str指向一個對象,支援類似

str.length(),str.replace()

等一切對象通路的操作。

test

的内容寫在

Class

檔案中僅僅解決的是如果指派的問題,那String對象是如何在記憶體中存在呢?

String建立過程

打開

java.lang.String

檔案,可以看到

String

擁有不可變對象的所有特點,

final

修飾的類,

final

修飾的成員變量,是以任何看似對String内容進行操作的方法,實際上都是傳回了一個新的String對象,這就造就了一個String對象的被建立後,就一直會保持不變。

正因為

String

這樣的特點,我們可以建立一個對String的對象的緩存池:

String Pool

,用來緩存所有第一次出現的

String

對象。

JVM規範中隻要求了

String Pool

,但并沒有要求如何實作,在

Hot Spot JVM

中,是通過類似一個

HashSet<String>

實作,裡面存儲是目前已存儲的String對象的引用:
String str="test";
           

首先虛拟機會在

String Pool

中查找是否有

equals("test")

的String 引用,如果有,就把字元串常量池裡面對

"test"

對象的引用指派給

str

。如果不存在,就在堆中建立一個"test"對象,并将引用駐留在字元串常量池(

String Pool

)中,同時将該引用複制給

str

可以看到,Java在這裡是使用的String緩存對象來解決“字面值”性能這個問題的。也就是說,"test"所對應的字面值其實是一個在字元串常量池的String對象這樣做隻要出現過一次的String對象,第二次就不再會被建立,節約了很大一筆開銷,便解決了String類似基本資料類型的性能問題。

深入了解String

明白了String的前因後果,現在來梳理關于String的細節問題。

String str="test"
           

包含了3個“值”:

  • "test"

    字面量,表示String對象所存儲的内容,編譯後存放在Class位元組碼中,運作時存放在

    Class

    對象中,而

    Class

    對象存儲在JVM的方法區中
  • test

    對象,存儲在堆中
  • test

    對象對應的引用,存儲在

    String Pool

    中。

如圖所示:

  1. 一定注意str所指向的對象是存放在堆中的,網上大多數說的不明白,更有誤導

    String Pool

    中存儲的是對象的說法。Java 對象,排除逃逸分析的優化,所有對象都是存儲在堆中的。
  2. String Pool

    位于JVM 的

    None Heap

    中,并且

    String Pool

    中的引用持有對堆中對應String對象的引用,是以不必擔心堆中的String對象是被GC回收。
  3. 網上很多文章還會說

    test

    字面值是存在

    Perm Gen

    中,但是這樣并不準确,永生代(“Perm Gen”)隻是Sun JDK的一個實作細節而已,Java語言規範和Java虛拟機規範都沒有規定必須有“Permanent Generation”這麼一塊空間,甚至沒規定要用什麼GC算法——不用分代式GC算法哪兒來的“永生代”? HotSpot的PermGen是用來實作Java虛拟機規範中的“方法區”(method area)的。
  4. 前面說過,Java想将String向基本資料類型靠近,還能展現在對

    final String

    對象的處理,對于

    final String

    ,如果使用僅僅是字面值的作用,而并沒有涉及到對象操作的話(使用對象通路操作符"."),編譯器會直接将對應的值替換為相應字面值。舉例:

    對于

    final String str="hello";
    String helloWorld=str+"world";
               
    編譯器會直接優化:
    String helloWorld="helloworld";
               
    final String str="hello";
    String hello=String.valueOf(str); 
               
    編譯器會直接優化
    String hello=String.valueOf("hello"); 
               

如果沒有編譯器的優化,就會涉及到操作數壓棧出棧等操作,但是經過優化後的String,可以發現并不會有astore/aload等指令的出現.

new String()

其實

new String

沒什麼好說的,

new String()

表示将

String

完全作為一個對象來看,放棄它的基本資料類型性質,也與

String Pool

沒有任何關系,但是

String

包含的

intern()

方法能将它與

String Pool

關聯起來。

  • jdk 1.7之前,

    intern()

    表示若

    String Pool

    中不存在該字元串,則 在堆中建立一個與調用

    intern()

    對象的字面值相同的對象,并在

    String Pool

    中儲存該對象的引用,同時傳回該對象,若存在則直接傳回。
  • jdk 1.7及1.7 之後,

    intern()

    表示将調用

    intern()

    對象的引用直接複制一份到

    String Pool

網上很多讨論涉及到幾個對象

String str=new String("hello world");
           

下面圖解分析:

需要明白的一點的是

new String("hello world")

并不是一個原子操作,可以将其分為兩步,每個關鍵字負責不同的工作其中

new

負責生成對象,

String("hello world")

負責初始化

new

生成的對象。
  • 首先,執行

    new

    操作,在堆中配置設定空間,并生成一個

    String

  • 其次,将

    new

    生成的對象的引用傳遞給

    String("hello world")

    方法進行初始化,而此時參數中出現了

    "hello world"

    字面量,JVM會先在字元串常量池裡面檢查是否有

    equals("hello world")

    的引用,如果沒有,就在堆中建立相應的對象,并生成一個引用指向這個對象,并将此引用存儲在

    字元串常量池

  1. 再次,複制常量池

    hello world

    指向的字面量對象傳遞給

    new String("hello world")

    進行初始化。
第二點中提到了複制,其實最主要的就是複制

String

對象中

value

所指向的位址,也就是将方法區中的

"hello world"

的索引複制給新的對象,這也是為什麼上圖中,兩個對象都指向方法區中同一個位置

下面的

String str=new String("hello world")

進行反編譯的結果:

0: new           #2                  // class java/lang/String
       3: dup
       4: ldc           #3                  // String hello world
       6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
       9: astore_1
      10: return
           

大概的指令應該都能看到,解釋一下:

  • new 執行new 操作,在堆中配置設定記憶體
  • dup 将new 操作生成的對象壓棧
  • ldc 将String型常量值從常量池中推送至棧頂
  • invokespecial 調用

    new String()

    并傳入new 出來的對象了ldc的String值

ldc指令是什麼東西?

簡單地說,它用于将int、float或String型常量值從常量池中推送至棧頂,在這裡也能看到,JVM是将

String

和八大基本資料類型統一處理的。

ldc 還隐藏了一個操作:也就是"hello world"的resolve操作,也就是檢測“hello world”是否已經在常量池中存在的操作。

傳送門詳見:Java 中new String("字面量") 中 "字面量" 是何時進入字元串常量池的?

有個很神奇的坑,《深入了解JVM》中曾經提到過這個問題,不過周志明老師是拿的"java"作為舉例:

代碼如下(jdk 1.7)

```

public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        String str1 = new StringBuilder("計算機").append("軟體").toString();
        System.out.println(str1.intern() == str1);

        String str2 = new StringBuilder("ja").append("va").toString();
        System.out.println(str2.intern() == str2);
    }
}
```
結果 
true
false
           

不明白為什麼"java"字元串會在執行StringBuilder.toString()之前出現過?

其實是因為:Java 标準庫在JVM啟動的時候加載的某些類已經包含了

java

字面量。

傳送門:如何了解《深入了解java虛拟機》第二版中對String.intern()方法的講解中所舉的例子?

方法區

上面圖中說了,

“hello wold”

對象的

value

的值是放在方法區中的。如何證明呢?

這裡我們可以使用反射來幹一些壞事。

雖然

String

類是一個不可變對象,對外并沒有提供如何修改

value

的方法,但是反射可以。

String str1=new String("abcd");
        String str2="abcd";
        String str3="abcd";
        Field valueField = String.class.getDeclaredField("value");
        valueField.setAccessible(true);//設定通路權限
        char[] value = (char[]) valueField.get(str2);
        value[0] = '0';
        value[1] = '1';
        value[2] = '2';
        value[3] = '3';

        System.out.println(str1);
        System.out.println(str2);
        System.out.println(str3);
        String str4="abcd";
        System.out.println(str4);
        System.out.println("abcd");
           

可以試一試,輸出結果都是

0123

,因為在編譯的時候生成

Class

對象的時候,

str1,str2,str3,str4

都是指向的

Class

檔案中同一個位置,而在運作的時候這個

Class

對象的值被修改後,所有和

abcd

有關的對象的

value

都會被改變。

相信了解了這一個例子的同學,能夠對

String

有一個更加深刻的了解

檢驗

說了這麼多,你真的懂了麼?來看看經常出現的一些關于

String

的問題:

String str1 = new StringBuilder("Hel").append("lo").toString();
System.out.println(str1.intern() == str1); 
String str = "Hello";
System.out.println(str == str1); 
           
String str1="hello";
String str2=new String("hello");
System.out.println(str2 == str1); 
           
final String str1="hell";
String str2="hello";
String str3=str1+"o";
System.out.println(str2 == str3); 
           
String str1="hell";
String str2="hello";
String str3=str1+"o";
System.out.println(str2 == str3); 
           
尊重原創,轉載注明出處

參考文章:

《深入了解JVM》 第二版

java用這樣的方式生成字元串:String str = "Hello",到底有沒有在堆中建立對象?

R大:請别再拿“String s = new String("xyz");建立了多少個String執行個體”來面試了吧

Java 中new String("字面量") 中 "字面量" 是何時進入字元串常量池的?