天天看點

java string了解_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 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中。

如圖所示:

java string了解_Java 幹貨之深入了解String

其中

一定注意str所指向的對象是存放在堆中的,網上大多數說的不明白,更有誤導String Pool中存儲的是對象的說法。Java 對象,排除逃逸分析的優化,所有對象都是存儲在堆中的。

String Pool位于JVM 的None Heap中,并且String Pool中的引用持有對堆中對應String對象的引用,是以不必擔心堆中的String對象是被GC回收。

網上很多文章還會說test字面值是存在Perm Gen中,但是這樣并不準确,永生代(“Perm Gen”)隻是Sun JDK的一個實作細節而已,Java語言規範和Java虛拟機規範都沒有規定必須有“Permanent Generation”這麼一塊空間,甚至沒規定要用什麼GC算法——不用分代式GC算法哪兒來的“永生代”? HotSpot的PermGen是用來實作Java虛拟機規範中的“方法區”(method area)的。

前面說過,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");

下面圖解分析:

java string了解_Java 幹貨之深入了解String

需要明白的一點的是new String("hello world")并不是一個原子操作,可以将其分為兩步,每個關鍵字負責不同的工作其中new負責生成對象,String("hello world")負責初始化new

生成的對象。

首先,執行new操作,在堆中配置設定空間,并生成一個String對象。

其次,将new生成的對象的引用傳遞給String("hello world")方法進行初始化,而此時參數中出現了"hello world"字面量,JVM會先在字元串常量池裡面檢查是否有equals("hello world")的引用,如果沒有,就在堆中建立相應的對象,并生成一個引用指向這個對象,并将此引用存儲在字元串常量池中。

再次,複制常量池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."":(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字面量。

方法區

上面圖中說了,“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);

尊重原創,轉載注明出處