淺談Java String內幕(1)

佔小狼發表於2016-08-29

前言

String字串在Java應用中使用非常頻繁,只有理解了它在虛擬機器中的實現機制,才能寫出健壯的應用,本文使用的JDK版本為1.8.0_3。

常量池

Java程式碼被編譯成class檔案時,會生成一個常量池(Constant pool)的資料結構,用以儲存字面常量和符號引用(類名、方法名、介面名和欄位名等)。

很簡單的一段程式碼,通過命令 javap -verbose 檢視class檔案中 Constant pool 實現:

通過反編譯出來的位元組碼可以看出字串 “test” 在常量池中的定義方式:

在main方法位元組碼指令中,0 ~ 2行對應程式碼 String test = "test"; 由兩部分組成:ldc #2 和 astore_1。

1、Test類載入到虛擬機器時,”test”字串在Constant pool中使用符號引用symbol表示,當呼叫 ldc #2 指令時,如果Constant pool中索引 #2 的symbol還未解析,則呼叫C++底層的 StringTable::intern 方法生成char陣列,並將引用儲存在StringTable和常量池中,當下次呼叫 ldc #2 時,可以直接從Constant pool根據索引 #2獲取 “test” 字串的引用,避免再次到StringTable中查詢。

2、astore_1指令將”test”字串的引用儲存在區域性變數表中。

常量池的記憶體分配 在 JDK6、7、8中有不同的實現:
1、JDK6及之前版本中,常量池的記憶體在永久代PermGen進行分配,所以常量池會受到PermGen記憶體大小的限制。
2、JDK7中,常量池的記憶體在Java堆上進行分配,意味著常量池不受固定大小的限制了。
3、JDK8中,虛擬機器團隊移除了永久代PermGen。

字串初始化

字串可以通過兩種方式進行初始化:字面常量和String物件。

字面常量

通過 “javap -c” 命令檢視位元組碼指令實現:

淺談Java String內幕(1)

其中ldc指令將int、float和String型別的常量值從常量池中推送到棧頂,所以a和b都指向常量池的”java”字串。通過指令實現可以發現:變數a、b和c都指向常量池的 “java” 字串,表示式 “ja” + “va” 在編譯期間會把結果值”java”直接賦值給c。

String物件

這種情況下,a == c 成立麼?位元組碼實現如下:

淺談Java String內幕(1)

其中3 ~ 9行指令對應程式碼 String c = new String("java"); 實現:
1、第3行new指令,在Java堆上為String物件申請記憶體;
2、第7行ldc指令,嘗試從常量池中獲取”java”字串,如果常量池中不存在,則在常量池中新建”java”字串,並返回;
3、第9行invokespecial指令,呼叫構造方法,初始化String物件。

其中String物件中使用char陣列儲存字串,變數a指向常量池的”java”字串,變數c指向Java堆的String物件,且該物件的char陣列指向常量池的”java”字串,所以很顯然 a != c,如下圖所示:

淺談Java String內幕(1)

通過 “字面量 + String物件” 進行賦值會發生什麼?

這種情況下,c == d成立麼?位元組碼實現如下:

淺談Java String內幕(1)

其中6 ~ 21行指令對應程式碼 String c = a + b; 實現:
1、第6行new指令,在Java堆上為StringBuilder物件申請記憶體;
2、第10行invokespecial指令,呼叫構造方法,初始化StringBuilder物件;
3、第14、18行invokespecial指令,呼叫append方法,新增a和b字串;
4、第21行invokespecial指令,呼叫toString方法,生成String物件。

通過指令實現可以發現,字串變數的連線動作,在編譯階段會被轉化成StringBuilder的append操作,變數c最終指向Java堆上新建String物件,變數d指向常量池的”hello world”字串,所以 c != d。

不過有種特殊情況,當final修飾的變數發生連線動作時,虛擬機器會進行優化,將表示式結果直接賦值給目標變數:

指令實現如下:

淺談Java String內幕(1)

END。

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

淺談Java String內幕(1)

相關文章