本文主要介紹Java中與字串相關的一些內容,主要包括String類的實現及其不變性、String相關類(StringBuilder、StringBuffer)的實現 以及 字串快取機制的用法與實現。
String類的設計與實現
String類的核心邏輯是透過對char型陣列進行封裝來實現字串物件,但實現細節伴隨著Java版本的演進也發生過幾次變化。
Java 6
public final class String implements java。io。Serializable, Comparable
/** The value is used for character storage。 */
private final char value[];
/** The offset is the first index of the storage that is used。 */
private final int offset;
/** The count is the number of characters in the String。 */
private final int count;
/** Cache the hash code for the string */
private int hash; // Default to 0}
在Java 6中,String類有四個成員變數:char型陣列value、偏移量 offset、字元數量 count、雜湊值 hash。value陣列用來儲存字元序列, offset 和 count 兩個屬性用來定位字串在value陣列中的位置,hash屬性用來快取字串的hashCode。
使用offset和count來定位value陣列的目的是,可以高效、快速地共享value陣列,例如substring()方法返回的子字串是透過記錄offset和count來實現與原字串共享value陣列的,而不是重新複製一份。substring()方法實現如下:
String(int offset, int count, char value[]) {
this。value = value; // 直接複用原陣列
this。offset = offset;
this。count = count;}public String substring(int beginIndex, int endIndex) {
// …… 省略一些邊界檢查的程式碼 ……
return ((beginIndex == 0) && (endIndex == count)) ? this :
new String(offset + beginIndex, endIndex - beginIndex, value);}
但是這種方式卻很有可能會導致記憶體洩漏。例如在如下程式碼中:
String bigStr = new String(new char[100000]);String subStr = bigStr。substring(0,2);bigStr = null;
在bigStr被設定為null之後,其中的value陣列卻仍然被subStr所引用,導致垃圾回收器無法將其回收,結果雖然我們實際上僅僅需要2個字元的空間,但是實際卻佔用了100000個字元的空間。
在Java 6中,如果想要避免這種記憶體洩漏情況的發生,可以使用下面的方式:
String subStr = bigStr。substring(0,2) + “”;// 或者String subStr = new String(bigStr。substring(0,2));
在語句執行完之後,substring方法返回的匿名String物件由於沒有被別的物件引用,所以能夠被垃圾回收器回收,不會繼續引用bigStr中的value陣列,從而避免了記憶體洩漏。
Java 7 & Java 8
public final class String implements java。io。Serializable, Comparable
/** The value is used for character storage。 */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0}
在Java 7-Java 8中,Java 對 String 類做了一些改變。String 類中不再有 offset 和 count 兩個成員變量了。substring()方法也不再共享 value陣列,而是從指定位置重新複製一份value陣列,從而解決了使用該方法可能導致的記憶體洩漏問題。substring()方法實現如下:
public String(char value[], int offset, int count) {
// …… 省略一些邊界檢查的程式碼 ……
// 從原陣列複製
this。value = Arrays。copyOfRange(value, offset, offset+count); }public String substring(int beginIndex, int endIndex) {
// …… 省略一些邊界檢查的程式碼 ……
return ((beginIndex == 0) && (endIndex == value。length)) ? this
: new String(value, beginIndex, subLen);}
Java 9
public final class String implements java。io。Serializable, Comparable
/** The value is used for character storage。 */
private final byte[] value;
/** The identifier of the encoding used to encode the bytes in {@code value}。 */
private final byte coder;
/** Cache the hash code for the string */
private int hash; // Default to 0}
為了節省記憶體空間,Java 9中對String的實現方式做了最佳化,value成員變數從char[]型別改為了byte[]型別,同時新增了一個coder成員變數。我們知道Java中char型別佔用的是兩個位元組,對於只佔用一個位元組的字元(例如,a-z,A-Z)就顯得有點浪費,所以Java 9中將char[]改為byte[]來儲存字元序列,而新屬性 coder 的作用就是用來表示value陣列中儲存的是雙位元組編碼的字元還是單位元組編碼的字元。coder 屬性可以有 0 和 1 兩個值,0 代表 Latin-1(單位元組編碼),1 代表 UTF-16(雙位元組編碼)。在建立字串的時候如果判斷所有字元都可以用單位元組來編碼,則使用Latin-1來編碼以壓縮空間,否則使用UTF-16編碼。主要的建構函式實現如下:
String(char[] value, int off, int len, Void sig) {
if (len == 0) {
this。value = “”。value;
this。coder = “”。coder;
return;
}
if (COMPACT_STRINGS) {
byte[] val = StringUTF16。compress(value, off, len); // 嘗試壓縮字串,使用單位元組編碼儲存
if (val != null) { // 壓縮成功,可以使用單位元組編碼儲存
this。value = val;
this。coder = LATIN1;
return;
}
}
// 否則,使用雙位元組編碼儲存
this。coder = UTF16;
this。value = StringUTF16。toBytes(value, off, len);}
String類的不變性
我們注意到String類是用final修飾的;所有的屬性都是宣告為private的;並且除了hash屬性之外的其他屬性也都是用final修飾。這保證了:
String類由final修飾,所以無法透過繼承String類改變其語義;
所有的屬性都是宣告為private的, 所以無法在String外部
直接
訪問或修改其屬性;
除了hash屬性之外的其他屬性都是用final修飾,表示這些屬性在初始化賦值後不可以再修改。
上述的定義共同實現了String類一個重要的特性 ——
不變性
,即 String 物件一旦建立成功,就不能再對它進行任何修改。String提供的方法substring()、concat()、replace()等方法返回值都是新建立的String物件,而不是原來的String物件。
hash屬性不是final的原因是:String的hashCode並不需要在建立字串時立即計算並賦值,而是在hashCode()方法被呼叫時才需要進行計算。
為什麼String類要設計為不可變的?
保證 String 物件的安全性。String被廣泛用作JDK中作為引數、返回值,例如網路連線,開啟檔案,類載入,等等。如果 String 物件是可變的,那麼 String 物件將可能被惡意修改,引發安全問題。
執行緒安全。String類的不可變性天然地保證了其執行緒安全的特性。
保證了String物件的hashCode的不變性。String類的不可變性,保證了其hashCode值能夠在第一次計算後進行快取,之後無需重複計算。這使得String物件很適合用作HashMap等容器的Key,並且相比其他物件效率更高。
實現字串常量池。Java為字串物件設計了字串常量池來共享字串,節省記憶體空間。如果字串是可變的,那麼字串物件便無法共享。因為如果改變了其中一個物件的值,那麼其他物件的值也會相應發生變化。
與String類相關的類
除了String類之外,還有兩個與String類相關的的類:StringBuffer和StringBuilder,這兩個類可以看作是String類的可變版本,提供了對字串修改的各種方法。兩者的區別在於StringBuffer是執行緒安全的而StringBuilder不是執行緒安全的。
StringBuffer / StringBuilder的實現
StringBuffer和StringBuilder都是繼承自AbstractStringBuilder,AbstractStringBuilder利用可變的char陣列(Java 9之後改為為byte陣列)來實現對字串的各種修改操作。StringBuffer和StringBuilder都是呼叫AbstractStringBuilder中的方法來操作字串, 兩者區別在於StringBuffer類中對字串修改的方法都加了synchronized修飾,而StringBuilder沒有,所以StringBuffer是執行緒安全的,而StringBuilder並非執行緒安全的。
我們以Java 8為例,看一下AbstractStringBuilder類的實現:
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/** The value is used for character storage。 */
char[] value;
/** The count is the number of characters used。 */
int count;}
value陣列用來儲存字元序列,count則用來儲存value陣列中已經使用的字元數量,字串真實的內容是value陣列中[0,count)之間的字元序列,而[count,length)之間是
未使用
的空間。需要count屬性記錄已使用空間的原因是,AbstractStringBuilder中的value陣列並不是每次修改都會重新申請,而是會提前預分配一定的多餘空間,以此來減少重新分配陣列空間的次數。(這種做法類似於ArrayList的實現)。
value陣列擴容的策略是:當對字串進行修改時,如果當前的value陣列不滿足空間需求時,則會重新分配更大的value陣列,分配的陣列大小為min( 原陣列大小×2 + 2 , 所需的陣列大小 ),更加細節的邏輯可以參考如下程式碼:
private static final int MAX_ARRAY_SIZE = Integer。MAX_VALUE - 8;private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value。length << 1) + 2; //原陣列大小×2 + 2
if (newCapacity - minCapacity < 0) { // 如果小於所需空間大小,擴充套件至所需空間大小
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;}private int hugeCapacity(int minCapacity) {
if (Integer。MAX_VALUE - minCapacity < 0) { // overflow
throw new OutOfMemoryError();
}
return (minCapacity > MAX_ARRAY_SIZE)
? minCapacity : MAX_ARRAY_SIZE;}
當然AbstractStringBuilder也提供了trimToSize方法去釋放多餘的空間:
public void trimToSize() {
if (count < value。length) {
value = Arrays。copyOf(value, count);
}}
String物件的快取機制
因為String物件的使用廣泛,Java為String物件設計了快取機制,以提升時間和空間上的效率。在JVM的執行時資料區中存在一個字串常量池(String Pool),在這個常量池中維護了所有已經快取的String物件,當我們說一個String物件被快取(interned)了,就是指它進入了字串常量池。
我們透過解答下面三個問題來理解String物件的快取機制:
哪些String物件會被快取進字串常量池?
String物件被快取在哪裡,如何組織起來的?
String物件是什麼時候進入字串常量池的?
說明
: 如未特殊指明,本文中提及的JVM實現均指的是Oracle的HotSpot VM,並且不考慮 逃逸分析(escape analysis)、標量替換(scalar replacement)、無用程式碼消除(dead-code elimination)等最佳化手段,測試程式碼基於不新增任何額外JVM引數的情況下執行。
預備知識
為了更好的閱讀體驗,在解答上面三個問題前,希望讀者對以下知識點有簡單瞭解:
JVM執行時資料區
class檔案的結構
JVM基於棧的位元組碼解釋執行引擎
類載入的過程
Java中的幾種常量池
為了內容的完整性,我們對下文涉及較多的其中兩點做簡要介紹。
類載入的過程
類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期依次為:載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和解除安裝(Unloading)7個階段。其中驗證、準備、解析3個部分統稱為連線(Linking)。
載入、驗證、準備、初始化和解除安裝這5個階段的順序是確定的,類的載入過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之後再開始,這是為了支援Java語言的執行時繫結(也稱為動態繫結或晚期繫結)。
Java中的幾種常量池
1. class檔案中的常量池
我們知道java字尾的原始碼檔案會被javac編譯為class字尾的class檔案(位元組碼檔案)。在class檔案中有一部分內容是 常量池(Constant Pool) ,這個常量池中主要儲存兩大類常量:
程式碼中的字面量或者常量表達式的值;
符號引用,包括:類和介面的全限定名、欄位的名稱和描述符、方法的名稱和描述符等,歡迎關注GZH(java架構寶典)。
2. 執行時常量池
在JVM執行時資料區(Run-Time Data Areas)中,有一部分是執行時常量池(Run-Time Constant Pool),屬於方法區的一部分。執行時常量池是class檔案中每個類或者介面的常量池(Constant Pool )的執行時表示形式,class檔案的常量池中的內容會在類載入後進入方法區的執行時常量池。
3. 字串常量池
字串常量池(String Pool)也就是我們上文提到的用來快取String物件的常量池。 這個常量池是全域性共享的,屬於執行時資料區的一部分。
哪些String物件會被快取進字串常量池?
在Java中,有兩種字串會被快取到字串常量池中,一種是在程式碼中定義的字串字面量或者字串常量表達式,另一種是程式中主動呼叫String。intern()方法將當前String物件快取到字串常量池中。下面分別對兩種方式做簡要介紹。
1。 隱式快取 - 字串字面量 或者 字串常量表達式
之所以稱之為隱式快取是因為我們並不需要主動去編寫快取相關程式碼,編譯器和JVM會幫我們完成這部分工作。
字串字面量
第一種會被隱式快取的字串是
字串字面量
。字面量 是型別為原始型別、String型別、null型別的值在原始碼中的表示形式。例如:
int i = 100; // int 型別字面量double f = 10。2; // double 型別字面量boolean b = true; // boolean 型別字面量String s = “hello”; // String型別字面量Object o = null; // null型別字面量
字串字面量是由雙引號括起來的0個或者多個字元構成的。 Java會在執行過程中為字串字面量建立String物件並加入字串常量池中。例如上面程式碼中的“hello”就是一個字串字面量,在執行過程中會先 建立一個內容為“hello”的String物件,並快取到字串常量池中,再將s引用指向這個String物件。
關於字串字面量更加詳細的內容請參閱Java語言規範(JLS - 3。10。5。 String Literals)。
字串常量表達式
另外一種會被隱式快取的字串是
字串常量表達式
。常量表達式指的是表示簡單型別值或String物件的表示式,可以簡單理解為常量表達式就是在編譯期間就能確定值的表示式。字串常量表達式就是表示String物件的常量表達式。例如:
int a = 1 + 2;double d = 10 + 2。01;boolean b = true & false;String str1 = “abc” + 123;final int num = 456;String str2 = “abc” +456;
Java會在執行過程中為字串常量表達式建立String物件並加入字串常量池中。例如,上面的程式碼中,會分別建立“abc123”和“abc456”兩個String物件,這兩個String物件會被快取到字串常量池中,str1會指向常量池中值為“abc123”的String物件,str2會指向常量池中值為“abc456”的String物件。
關於常量表達式更加詳細的內容請參閱Java語言規範(JLS - 15。28 Constant Expressions)。
2。 主動快取 - String。intern()方法
除了宣告為字串字面量/字串常量表達式之外,透過其他方式得到的String物件也可以主動加入字串常量池中。例如:
String str = new String(“123”) + new String(“456”);str。intern();
在上面的程式碼中,在執行完第一句後,常量池中存在內容為“123”和“456”的兩個String物件,但是不存在“123456”的String物件,但在執行完str。intern();之後,內容為“123456”的String物件也加入到了字串常量池中。
我們透過String。intern()方法的註釋來看下其具體的快取機制:
When the intern method is invoked, if the pool already contains a string equal to this String object as determined by the equals(Object) method, then the string from the pool is returned。 Otherwise, this String object is added to the pool and a reference to this String object is returned。
It follows that for any two strings s and t, s。intern() == t。intern() is true if and only if s。equals(t) is true。
簡單翻譯一下:
當呼叫 intern 方法時,如果常量池中已經包含相同內容的字串(字串內容相同由 equals (Object) 方法確定,對於 String 物件來說,也就是字元序列相同),則返回常量池中的字串物件。否則,將此 String 物件將新增到常量池中,並返回此 String 物件的引用。
因此,對於任意兩個字串 s 和 t,當且僅當 s。equals(t)的結果為true時,s。intern() == t。intern()的結果為true。
String物件被快取在哪裡,如何組織起來的?
HotSpot VM中,有一個用來記錄快取的String物件的全域性表,叫做StringTable,結構及實現方式都類似於Java中的HashMap或者HashSet,是一個使用拉鍊法解決雜湊衝突的雜湊表,可以簡單理解為HashSet
而真正的字串物件其實是儲存在另外的區域中的,在Java 6中字串常量池中的String物件是儲存在永久代(Java 8之前HotSpot VM對方法區的實現)中的,而在Java 6之後,字串常量池中的String物件是儲存在堆中的。
Java 7中將字串常量池中的物件移動到堆中的原因是在 Java 6中,字串常量池中的物件在永久代建立,而永久代代的大小一般不會設定太大,如果大量使用字串快取將可能對導致永久代發生OOM異常。
String物件是什麼時候進入字串常量池的?
對於透過 在程式中呼叫String。intern()方法主動快取進入常量池的String物件,很顯然就是在呼叫intern()方法的時候進入常量池的。
我們重點來研究一下會被隱式快取的兩種值(字串字面量和字串常量表達式),主要是兩個問題:
我們並沒有主動呼叫String類的構造方法,那麼它們是在何時被建立?
它們是在何時進入字串常量池的?
我們以下面的程式碼為例來分析這兩個問題:
public class Main {
public static void main(String[] args) {
String str1 = “123” + 123; // 字串常量表達式
String str2 = “123456”; // 字面量
String str3 = “123” + 456; //字串常量表達式
}}
位元組碼分析
我們對上述程式碼編譯之後使用javap來觀察一下位元組碼檔案,為了節省篇幅,只摘取了相關的部分:常量池表部分以及main方法資訊部分:
Constant pool:
#1 = Methodref #5。#23 // java/lang/Object。“
#2 = String #24 // 123123
#3 = String #25 // 123456
// …… 省略 ……
#24 = Utf8 123123
#25 = Utf8 123456
// …… 省略 ……
public static void main(java。lang。String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC Code:
stack=1, locals=4, args_size=1
0: ldc #2 // String 123123
2: astore_1 3: ldc #3 // String 123456
5: astore_2 6: ldc #3 // String 123456
8: astore_3 9: return
LineNumberTable:
line 7: 0
line 8: 3
line 9: 6
line 10: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
3 7 1 str1 Ljava/lang/String;
6 4 2 str2 Ljava/lang/String;
9 1 3 str3 Ljava/lang/String;
在常量池中,有兩種與字串相關的常量型別,CONSTANT_String和CONSTANT_Utf8。CONSTANT_String型別的常量用於表示String型別的常量物件,其內容只是一個常量池的索引值index,index處的成員必須是CONSTANT_Utf8型別。而CONSTANT_Utf8型別的常量用於儲存真正的字串內容。
例如,上面的常量池中的第2、3項是CONSTANT_String型別,儲存的索引分別為24、25,常量池中第24、25項就是CONSTANT_Utf8,儲存的值分別為“123123”,“123456”。
class檔案的方法資訊中Code屬性是class檔案中最為重要的部分之一,其中包含了執行語句對應的虛擬機器指令,異常表,本地變數資訊等,其中LocalVariableTable是本地變數的資訊,Slot可以理解為本地變量表中的索引位置。ldc指令的作用是從執行時常量池中提取指定索引位置的資料並壓入棧中;astore_
ldc #
執行過程分析
還是圍繞上面的程式碼,我們結合 從編譯到執行的過程 來分析一下字串字面量和字串常量表達式的
建立
及
快取
時機。
1. 編譯
首先,第一步是javac將原始碼編譯為class檔案。在原始碼編譯過程中,我們上文提到的兩種值 字串字面量(“123456”) 和 字串常量表達式(“123” + 456)這兩類值都會存在編譯後的class檔案的常量池中,常量型別為CONSTANT_String。值得注意的兩點是:
字串常量表達式會在編譯期計算出真實值存在class檔案的常量池中。例如上面原始碼中的“123” + 123這個表示式在class檔案的常量池中的表現形式是123123,“123” + 456這個表示式在class檔案的常量池中的表現形式是123456;
值相同的字串字面量或者字串常量表達式在class檔案的常量池中只會存在一個常量項(CONSTANT_String型別和CONSTANT_Utf8都只有一項)。例如上面原始碼中,雖然聲明瞭兩個常量值分別為“123456”和“123” + 456,但是最後class檔案的常量池中只有一個值為123456的CONSTANT_Utf8常量項以及一個對應的CONSTANT_String常量項。
2. 類載入
在JVM執行時,載入Main類時,JVM會根據 class檔案的常量池 建立 執行時常量池, class檔案的常量池 中的內容會在類載入時進入方法區的 執行時常量池。對於class檔案的常量池中的符號引用,會在類載入的解析(resolve)階段,會將其轉化為真正的值。但在HotSpot中,符號引用的解析並不一定是在類載入時立即執行的,而是推遲到第一次執行相關指令(即引用了符號引用的指令,JLS - 5。4。3。 Resolution )時才會去真正進行解析,這就做延遲解析/惰性解析(“lazy” or “late” resolution)。
對於一些基本型別的常量項,例如CONSTANT_Integer_info,CONSTANT_Float_info,CONSTANT_Long_info,CONSTANT_Double_info,在類載入階段會將class檔案常量池中的值轉化為執行時常量池中的值,分別對應C++中的int,float,long,double型別;
對於CONSTANT_Utf8型別的常量項,在類載入的解析階段被轉化為Symbol物件(HotSpot VM層面的一個C++物件)。同時HotSpot使用SymbolTable(結構與StringTable類似)來快取Symbol物件,所以在類載入完成後,SymbolTable中應該有所有的CONSTANT_Utf8常量對應的Symbol物件;
而對於CONSTANT_String型別的常量項,因為其內容是一個符號引用(指向CONSTANT_Utf8型別常量的索引值),所以需要進行解析,在類載入的解析階段會將其轉化為java。lang。String物件對應的oop(可以理解為Java物件在HotSpot VM層面的表示),並使用StringTable來進行快取。但是CONSTANT_String型別的常量,屬於上文提到的延遲解析的範疇,也就是在類載入時並不會立即執行解析,而是等到第一次執行相關指令時(一般來說是ldc指令)才會真正解析。
3. 執行指令
上面提到,JVM會在第一次執行相關指令的時候去執行真正的解析,對於上文給出的程式碼,觀察位元組碼可以發現,ldc指令中使用到了符號引用,所以在執行ldc指令時,需要進行解析操作。那麼ldc指令到底做了什麼呢?
ldc指令會從執行時常量池中查詢指定index對應的常量項,並將其壓入棧中。如果該項還未解析,則需要先進行解析,將符號引用轉化為具體的值,然後再將其壓入棧中。如果這個未解析的項是String型別的常量,則先從字串常量池中查詢是否已經有了相同內容的String物件,如果有則直接將字串常量池中的該物件壓入棧中;如果沒有,則會建立一個新的String物件加入字串常量池中,並將建立的新物件壓入棧中。可見,如果程式碼中宣告多個相同內容的字串字面量或者字串常量表達式,那麼只會在第一次執行ldc指令時建立一個String物件,後續相同的ldc指令執行時相應位置的常量已經解析過了,直接壓入棧中即可。
總結一下:
在編譯階段,原始碼中字串字面量或者字串常量表達式轉化為了class檔案的常量池中的CONSTANT_String常量項。
在類載入階段,class檔案的常量池中的CONSTANT_String常量項被存入了執行時常量池中,但儲存的內容仍然是一個符號引用,未進行解析。
在指令執行階段,當第一次執行ldc指令時,執行時常量池中的CONSTANT_String項還未解析,會真正執行解析,解析過程中會建立String物件並加入字串常量池。
快取關鍵原始碼分析
可以看到,其實ldc指令在解析String型別常量的時候與String。intern()方法的邏輯很相似:
ldc指令中解析String常量:先從字串常量池中查詢是否有相同內容的String物件,如果有則將其壓入棧中,如果沒有,則建立新物件加入字串常量池並壓入棧中。
String。intern()方法:先從字串常量池中查詢是否有相同內容的String物件,如果有則返回該物件引用,如果沒有,則將自身加入字串常量池並返回。
實際在HotSpot內部實現上,ldc指令 與 String。intern()對應的native方法 呼叫了相同的內部方法。我們以OpenJDK 8的原始碼為例,簡單分析一下其過程,程式碼如下(原始碼位置:src/share/vm/classfile/SymbolTable。cpp):
// String。intern()方法會呼叫這個方法// 引數 “oop string”代表呼叫intern()方法的String物件oop StringTable::intern(oop string, TRAPS){
if (string == NULL) return NULL;
ResourceMark rm(THREAD);
int length;
Handle h_string (THREAD, string);
jchar* chars = java_lang_String::as_unicode_string(string, length, CHECK_NULL); // 將String物件轉化為字元序列
oop result = intern(h_string, chars, length, CHECK_NULL);
return result;}// ldc指令執行時會呼叫這個方法// 引數 “Symbol* symbol” 是 執行時常量池 中 ldc指令的引數(索引位置)對應位置的Symbol物件oop StringTable::intern(Symbol* symbol, TRAPS) {
if (symbol == NULL) return NULL;
ResourceMark rm(THREAD);
int length;
jchar* chars = symbol->as_unicode(length); // 將Symbol物件轉化為字元序列
Handle string;
oop result = intern(string, chars, length, CHECK_NULL);
return result;}// 上面兩個方法都會呼叫這個方法oop StringTable::intern(Handle string_or_null, jchar* name, int len, TRAPS) {
// 嘗試從字串常量池中尋找
unsigned int hashValue = hash_string(name, len);
int index = the_table()->hash_to_index(hashValue);
oop found_string = the_table()->lookup(index, name, len, hashValue);
// 如果找到了直接返回
if (found_string != NULL) {
ensure_string_alive(found_string);
return found_string;
}
// …… 省略部分程式碼 ……
Handle string;
// 嘗試複用原字串,如果無法複用,則會建立新字串
// JDK 6中這裡的實現有一些不同,只有string_or_null已經存在於永久代中才會複用
if (!string_or_null。is_null()) {
string = string_or_null;
} else {
string = java_lang_String::create_from_unicode(name, len, CHECK_NULL);
}
//…… 省略部分程式碼 ……
oop added_or_found;
{
MutexLocker ml(StringTable_lock, THREAD);
// 新增字串到 StringTable 中
added_or_found = the_table()->basic_add(index, string, name, len,
hashValue, CHECK_NULL);
}
ensure_string_alive(added_or_found);
return added_or_found;}
案例分析
說明
:因為在Java 6之後字串常量池從永久代移到了堆中,可能在一些程式碼上Java 6與之後的版本表現不一致。所以下面的程式碼都使用Java 6和Java 7分別進行測試,如果未特殊說明,表示在兩個版本上結果相同,如果不同,會單獨指出。
final int a = 4;int b = 4;String s1 = “123” + a + “567”;String s2 = “123” + b + “567”;String s3 = “1234567”;System。out。println(s1 == s2);System。out。println(s1 == s3);System。out。println(s2 == s3);
結果:
falsetruefalse
解釋:
第三行,因為a被定義為常量,所以“123” + a + “567”是一個常量表達式,在編譯期會被編譯為“1234567”,所以會在字串常量池中建立“1234567”,s1指向字串常量池中的“1234567”;
第四行,b被定義為變數,“123”和“567”是字串字面量,所以首先在字串常量池中建立“123”和“567”,然後透過StringBuilder隱式拼接在堆中建立“1234567”,s2指向堆中的“1234567”;
第五行,“1234567”是一個字串字面量,因為此時字串常量池中已經存在了“1234567”,所以s3指向字串字串常量池中的“1234567”。
String s1 = new String(“123”);String s2 = s1。intern();String s3 = “123”;System。out。println(s1 == s2); System。out。println(s1 == s3); System。out。println(s2 == s3);
結果:
falsefalsetrue
解釋:
第一行,“123”是一個字串字面量,所以首先在字串常量池中建立了一個“123”物件,然後使用String的建構函式在堆中建立了一個“123”物件,s1指向堆中的“123”;
第二行,因為字串常量池中已經有了“123”,所以s2指向字串常量池中的“123”;
第三行,同樣因為字串常量池中已經有了“123”,所以s3指向字串常量池中的“123”。
String s1 = String。valueOf(“123”);String s2 = s1。intern();String s3 = “123”;System。out。println(s1 == s2); System。out。println(s1 == s3); System。out。println(s2 == s3);
結果:
truetruetrue
解釋:與上一種情況的區別在於,String。valueOf()方法在引數為String物件的時候會直接將引數作為返回值,不會在堆上建立新物件,所以s1也指向字串常量池中的“123”,三個變數指向同一個物件。
String s1 = new String(“123”) + new String(“456”); String s2 = s1。intern();String s3 = “123456”;System。out。println(s1 == s2); System。out。println(s1 == s3); System。out。println(s2 == s3);
上面的程式碼在Java 6和Java 7中結果是不同的。
在Java 6中:
falsefalsetrue
解釋:
第一行,“123”和“456”是字串字面量,所以首先在字串常量池中建立“123”和“456”,+運算子透過StringBuilder隱式拼接在堆中建立“123456”,s1指向堆中的“123456”;
第二行,將“123456”快取到字串常量池中,因為Java 6中字串常量池中的物件是在永久代建立的,所以會在字串常量池(永久代)建立一個“123456”,此時在堆中和永久代中各有一個“123456”,s2指向字串常量池(永久代)中的“123456”;
第三行,“123456”是字串字面量,因為此時字串常量池(永久代)中已經存在“123456”,所以s3指向字串常量池(永久代)中的“123456”。
在Java 7中:
truetruetrue
解釋:與Java 6的區別在於,因為Java 7中字串常量池中的物件是在堆上建立的,所以當執行第二行String s2 = s1。intern();時不會再建立新的String物件,而是直接將s1的引用新增到StringTable中,所以三個物件都指向常量池中的“123456”,也就是第一行中在堆中建立的物件。
Java 7下,s1 == s2結果為true也能夠用來佐證我們上面延遲解析的過程。我們假設如果“123456”不是延遲解析的,而是類載入的時候解析完成並進入常量池的,s1。intern()的返回值應該是常量池中存在的“123456”,而不會將s1指向的堆中的“123456”物件加入常量池,所以結果應該是s2不等於s1而等於s3。
String s1 = new String(“123”) + new String(“456”);String s2 = “123456”;String s3 = s1。intern();System。out。println(s1 == s2); System。out。println(s1 == s3); System。out。println(s2 == s3);
結果:
falsefalsetrue
解釋:
第一行,“123”和“456”是字串字面量,所以首先在字串常量池中建立“123”和“456”,+運算子透過StringBuilder隱式拼接在堆中建立“123456”,s1指向堆中的“123456”;
第二行,“123456”是字串字面量,此時字串常量池中不存在“123456”,所以在字串常量池中建立“123456”, s2指向字串常量池中的“123456”;
第三行,因為此時字串常量池中已經存在“123456”,所以s3指向字串常量池中的“123456”。
來自:GZH java架構寶典