可以證明,字符串操作是計算機程序設計中最常見的行為,尤其是在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
中。
如圖所示:
其中
-
一定注意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");
下面圖解分析:
需要明白的一點的是
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."<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("字面量") 中 "字面量" 是何時進入字符串常量池的?