簡書 占小狼
轉(zhuǎn)載請注明原創(chuàng)出處彬碱,謝謝!
前言
String字符串在Java應用中使用非常頻繁婴程,只有理解了它在虛擬機中的實現(xiàn)機制晋辆,才能寫出健壯的應用渠脉,本文使用的JDK版本為1.8.0_3。
常量池
Java代碼被編譯成class文件時瓶佳,會生成一個常量池(Constant pool)的數(shù)據(jù)結(jié)構(gòu)芋膘,用以保存字面常量和符號引用(類名、方法名霸饲、接口名和字段名等)为朋。
package com.ctrip.ttd.whywhy;
public class Test {
public static void main(String[] args) {
String test = "test";
}
}
很簡單的一段代碼,通過命令 javap -verbose
查看class文件中 Constant pool 實現(xiàn):
Constant pool:
#1 = Methodref #4.#13 // java/lang/Object."<init>":()V
#2 = String #14 // test
#3 = Class #15 // com/ctrip/ttd/whywhy/test
#4 = Class #16 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 main
#10 = Utf8 ([Ljava/lang/String;)V
#11 = Utf8 SourceFile
#12 = Utf8 test.java
#13 = NameAndType #5:#6 // "<init>":()V
#14 = Utf8 test
#15 = Utf8 com/ctrip/ttd/whywhy/test
#16 = Utf8 java/lang/Object
通過反編譯出來的字節(jié)碼可以看出字符串 "test" 在常量池中的定義方式:
#2 = String #14 // test
#14 = Utf8 test
在main方法字節(jié)碼指令中厚脉,0 ~ 2行對應代碼 String test = "test";
由兩部分組成:ldc #2 和 astore_1习寸。
// main方法字節(jié)碼指令
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String test
2: astore_1
3: return
1、Test類加載到虛擬機時傻工,"test"字符串在Constant pool中使用符號引用symbol表示融涣,當調(diào)用 ldc #2
指令時,如果Constant pool中索引 #2 的symbol還未解析精钮,則調(diào)用C++底層的 StringTable::intern
方法生成char數(shù)組,并將引用保存在StringTable和常量池中剃斧,當下次調(diào)用 ldc #2
時轨香,可以直接從Constant pool根據(jù)索引 #2獲取 "test" 字符串的引用,避免再次到StringTable中查找幼东。
2臂容、astore_1指令將"test"字符串的引用保存在局部變量表中。
常量池的內(nèi)存分配 在 JDK6根蟹、7脓杉、8中有不同的實現(xiàn):
1、JDK6及之前版本中简逮,常量池的內(nèi)存在永久代PermGen進行分配球散,所以常量池會受到PermGen內(nèi)存大小的限制。
2散庶、JDK7中蕉堰,常量池的內(nèi)存在Java堆上進行分配,意味著常量池不受固定大小的限制了悲龟。
3屋讶、JDK8中,虛擬機團隊移除了永久代PermGen须教。
字符串初始化
字符串可以通過兩種方式進行初始化:字面常量和String對象皿渗。
字面常量
public class StringTest {
public static void main(String[] args) {
String a = "java";
String b = "java";
String c = "ja" + "va";
}
}
通過 "javap -c" 命令查看字節(jié)碼指令實現(xiàn):
其中l(wèi)dc指令將int、float和String類型的常量值從常量池中推送到棧頂,所以a和b都指向常量池的"java"字符串乐疆。通過指令實現(xiàn)可以發(fā)現(xiàn):變量a划乖、b和c都指向常量池的 "java" 字符串,表達式 "ja" + "va" 在編譯期間會把結(jié)果值"java"直接賦值給c诀拭。
String對象
public class StringTest {
public static void main(String[] args) {
String a = "java";
String c = new String("java");
}
}
這種情況下迁筛,a == c 成立么?字節(jié)碼實現(xiàn)如下:
其中3 ~ 9行指令對應代碼
String c = new String("java");
實現(xiàn):1耕挨、第3行new指令细卧,在Java堆上為String對象申請內(nèi)存;
2筒占、第7行l(wèi)dc指令贪庙,嘗試從常量池中獲取"java"字符串,如果常量池中不存在翰苫,則在常量池中新建"java"字符串止邮,并返回;
3奏窑、第9行invokespecial指令导披,調(diào)用構(gòu)造方法,初始化String對象埃唯。
其中String對象中使用char數(shù)組存儲字符串撩匕,變量a指向常量池的"java"字符串,變量c指向Java堆的String對象墨叛,且該對象的char數(shù)組指向常量池的"java"字符串止毕,所以很顯然 a != c,如下圖所示:
**通過 "字面量 + String對象" 進行賦值會發(fā)生什么漠趁? **
public class StringTest {
public static void main(String[] args) {
String a = "hello ";
String b = "world";
String c = a + b;
String d = "hello world";
}
}
這種情況下扁凛,c == d成立么?字節(jié)碼實現(xiàn)如下:
其中6 ~ 21行指令對應代碼
String c = a + b;
實現(xiàn):1闯传、第6行new指令谨朝,在Java堆上為StringBuilder對象申請內(nèi)存;
2甥绿、第10行invokespecial指令叠必,調(diào)用構(gòu)造方法,初始化StringBuilder對象妹窖;
3纬朝、第14、18行invokespecial指令骄呼,調(diào)用append方法共苛,添加a和b字符串判没;
4、第21行invokespecial指令隅茎,調(diào)用toString方法澄峰,生成String對象。
通過指令實現(xiàn)可以發(fā)現(xiàn)辟犀,字符串變量的連接動作俏竞,在編譯階段會被轉(zhuǎn)化成StringBuilder的append操作,變量c最終指向Java堆上新建String對象堂竟,變量d指向常量池的"hello world"字符串魂毁,所以 c != d。
不過有種特殊情況出嘹,當final修飾的變量發(fā)生連接動作時席楚,虛擬機會進行優(yōu)化,將表達式結(jié)果直接賦值給目標變量:
public class StringTest {
public static void main(String[] args) {
final String a = "hello ";
final String b = "world";
String c = a + b;
String d = "hello world";
}
}
指令實現(xiàn)如下:
END税稼。
我是占小狼烦秩。
在魔都艱苦奮斗,白天是上班族郎仆,晚上是知識服務工作者只祠。
如果讀完覺得有收獲的話,記得關(guān)注和點贊哦扰肌。
非要打賞的話抛寝,我也是不會拒絕的。