這篇文章主要是做一個(gè)總結(jié)烘贴,將查找到相關(guān)的資料自己做一個(gè)整理,最后會(huì)列出查找過的相關(guān)資料,感興趣的可以去翻一翻刺洒。
常量池
-
class文件常量池(class constant pool)
常量池可以理解為Class文件之中的資源倉庫,它是Class文件結(jié)構(gòu)中與其他項(xiàng)目關(guān)聯(lián)最多的數(shù)據(jù)類型亚斋,包含了類也是占用Class文件中第一個(gè)出現(xiàn)的表類型數(shù)據(jù)項(xiàng)目作媚。
常量池中主要存放兩大類常量:字面量(Literal)和符號(hào)引用(Symbolic References)。字面量比較接近于Java語言層面的常量概念帅刊,如文本字符串、聲明為final的常量值等漂问。而符號(hào)引用則屬于編譯原理方面的概念赖瞒,包含了下面三類常量:
- 類和接口的全限定名(Full Qualified Name)
- 字段的名稱和描述符(Descriptor)
- 方法的名稱和描述符
類和接口的全限定名,例如:
com/example/demo/Demo.class
字段的名稱和描述符蚤假,例如:
Field a:[Ljava/lang/String
方法的名稱和描述符栏饮,例如:
Method java/lang/String."<init>":(Ljava/lang/String;)V
后兩個(gè)是字節(jié)碼指令,不懂得可以查閱下相關(guān)資料(TODO)
可以通過查看字節(jié)碼的形式來查看Class的常量池的內(nèi)容磷仰,因?yàn)槭窃诰幾g時(shí)產(chǎn)生的袍嬉,也可以稱為
靜態(tài)常量池
。
public class Main {
private int a=1;
private int b=1;
private Aload c=new Aload();
private String [] d =new String[10];
public static void main(String[] args) {
}
}
字節(jié)碼:
public class com.verzqli.snake.Main
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool: //這里就是class文件的常量池
#1 = Methodref #10.#30 // java/lang/Object."<init>":()V
#2 = Fieldref #9.#31 // com/verzqli/snake/Main.a:I
#3 = Fieldref #9.#32 // com/verzqli/snake/Main.b:I
#4 = Class #33 // com/verzqli/snake/Aload
#5 = Methodref #4.#30 // com/verzqli/snake/Aload."<init>":()V
#6 = Fieldref #9.#34 // com/verzqli/snake/Main.c:Lcom/verzqli/snake/Aload;
#7 = Class #35 // java/lang/String
#8 = Fieldref #9.#36 // com/verzqli/snake/Main.d:[Ljava/lang/String;
#9 = Class #37 // com/verzqli/snake/Main
#10 = Class #38 // java/lang/Object
#11 = Utf8 a
#12 = Utf8 I
#13 = Utf8 b
#14 = Utf8 c
#15 = Utf8 Lcom/verzqli/snake/Aload;
#16 = Utf8 d
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 <init>
#19 = Utf8 ()V
#20 = Utf8 Code
#21 = Utf8 LineNumberTable
#22 = Utf8 LocalVariableTable
#23 = Utf8 this
#24 = Utf8 Lcom/verzqli/snake/Main;
#25 = Utf8 main
#26 = Utf8 ([Ljava/lang/String;)V
#27 = Utf8 args
#28 = Utf8 SourceFile
#29 = Utf8 Main.java
#30 = NameAndType #18:#19 // "<init>":()V
#31 = NameAndType #11:#12 // a:I
#32 = NameAndType #13:#12 // b:I
#33 = Utf8 com/verzqli/snake/Aload
#34 = NameAndType #14:#15 // c:Lcom/verzqli/snake/Aload;
#35 = Utf8 java/lang/String
#36 = NameAndType #16:#17 // d:[Ljava/lang/String;
#37 = Utf8 com/verzqli/snake/Main
#38 = Utf8 java/lang/Object
-
運(yùn)行時(shí)常量池
當(dāng)java文件被編譯成class文件之后,就會(huì)生成上面的常量池伺通,在Class文件中描述的各種信息箍土,最終都需要加載到虛擬機(jī)中之后才能運(yùn)行和使用。
類從被加載到虛擬機(jī)內(nèi)存中開始罐监,到卸載出內(nèi)存位置吴藻,他的生命周期包括:加載(Loading)、驗(yàn)證(Verification)弓柱、準(zhǔn)備(Preparation)沟堡、解析(Resolution)、初始化(Initalization)矢空、使用(Using)和卸載(Unloading),其中驗(yàn)證航罗、準(zhǔn)備、解析三個(gè)部分統(tǒng)稱Wie連接(Linking)屁药。而當(dāng)類加載到內(nèi)存中后伤哺,JVM就會(huì)將Class常量池中的內(nèi)容存放到運(yùn)行時(shí)常量池中,由此可知者祖,運(yùn)行時(shí)常量池也是每個(gè)類都有一個(gè)立莉。在解析過程中需要將常量池中所有的符號(hào)引用(
classes、interfaces七问、fields蜓耻、methods referenced in the constant pool
)轉(zhuǎn)為直接引用(得到類或者字段、方法在內(nèi)存中的指針或者偏移量械巡,以便直接調(diào)用該方法
)刹淌。直接引用可以是內(nèi)存中,直接指向目標(biāo)的指讥耗、相對(duì)偏移量有勾,或是一個(gè)能間接定位到目標(biāo)的句柄,解析的這個(gè)階段其實(shí)就是將符號(hào)引用轉(zhuǎn)換為可以直接定位對(duì)象等在內(nèi)存中的位置的直接引用古程。運(yùn)行時(shí)常量池位于JVM規(guī)范的方法區(qū)中蔼卡,在Java8以前,位于永生代挣磨;Java8之后位于元空間雇逞。
-
全局字符串常量池(string pool / string literal pool)
全局字符串池里的內(nèi)容是在類加載完成,經(jīng)過驗(yàn)證茁裙,準(zhǔn)備階段之后在堆中生成字符串對(duì)象實(shí)例塘砸,然后將該字符串對(duì)象實(shí)例的引用值存到string pool中。在HotSpot中具體實(shí)現(xiàn)string pool這一功能的是StringTable類晤锥,它是一個(gè)哈希表掉蔬,里面存的是key(字面量“abc”, 即駐留字符串)-value(字符串"abc"實(shí)例對(duì)象在堆中的引用)鍵值對(duì)廊宪,StringTable本身存在本地內(nèi)存(native memory)中。
StringTable在每個(gè)HotSpot VM的實(shí)例只有一份女轿,被所有的類共享(享元模式)箭启。在Java7的時(shí)候?qū)⒆址A砍匾频搅硕牙铮瑫r(shí)里面也不在存放對(duì)象(Java7以前被intern的String對(duì)象存放于永生代谈喳,所以很容易造成OOM)册烈,而是存放堆上String實(shí)例對(duì)象的引用。
那么字符串常量池中引用的String對(duì)象是在什么時(shí)候創(chuàng)建的呢婿禽?在JVM規(guī)范里明確指定resolve階段可以是lazy的赏僧,即在需要進(jìn)行該符號(hào)引用的解析時(shí)才去解析它,這樣的話扭倾,可能該類都已經(jīng)初始化完成了淀零,如果其他的類鏈接到該類中的符號(hào)引用,需要進(jìn)行解析膛壹,這個(gè)時(shí)候才會(huì)去解析驾中。
這時(shí)候就需要ldc
這個(gè)字節(jié)碼指令,其作用是將int模聋、float或String型常量值從常量池中推送至棧頂,如下面這個(gè)例子肩民。
public class Main {
public static void main(String[] args) {
String a="B";
}
}
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: ldc #2 // String B
2: astore_1
3: return
LineNumberTable:
line 14: 0
line 15: 3
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 args [Ljava/lang/String;
3 1 1 a Ljava/lang/String;
}
在main方法的字節(jié)碼中使用ldc將字符串“B”推到棧頂,然后賦值給局部變量a,最后退出链方。
根據(jù)上面說的持痰,在類加載階段,這個(gè) resolve 階段( constant pool resolution )是lazy的祟蚀。換句話說并沒有真正的對(duì)象工窍,字符串常量池里自然也沒有,那么ldc指令還怎么把人推送至棧頂前酿?或者換一個(gè)角度想患雏,既然resolve 階段是lazy的,那總有一個(gè)時(shí)候它要真正的執(zhí)行吧罢维,是什么時(shí)候淹仑?執(zhí)行l(wèi)dc指令就是觸發(fā)這個(gè)lazy resolution動(dòng)作的條件。
ldc字節(jié)碼在這里的執(zhí)行語義是:到當(dāng)前類的運(yùn)行時(shí)常量池(runtime constant pool言津,HotSpot VM里是ConstantPool + ConstantPoolCache)去查找該index對(duì)應(yīng)的項(xiàng)攻人,如果該項(xiàng)尚未resolve則resolve之,并返回resolve后的內(nèi)容悬槽。
在遇到String類型常量時(shí),resolve的過程如果發(fā)現(xiàn)StringTable已經(jīng)有了內(nèi)容匹配的java.lang.String的引用瞬浓,則直接返回這個(gè)引用初婆,反之,如果StringTable里尚未有內(nèi)容匹配的String實(shí)例的引用,則會(huì)在Java堆里創(chuàng)建一個(gè)對(duì)應(yīng)內(nèi)容的String對(duì)象磅叛,然后在StringTable記錄下這個(gè)引用屑咳,并返回這個(gè)引用出去”浊伲可見兆龙,ldc指令是否需要?jiǎng)?chuàng)建新的String實(shí)例,全看在第一次執(zhí)行這一條ldc指令時(shí)敲董,StringTable是否已經(jīng)記錄了一個(gè)對(duì)應(yīng)內(nèi)容的String的引用紫皇。
public class Main {
String a="b";
public static void main(String[] args) {
}
}
public com.verzqli.snake.Main();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String b
7: putfield #3 // Field a:Ljava/lang/String;
10: return
LineNumberTable:
line 12: 0
line 13: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/verzqli/snake/Main;
上面例子執(zhí)行完main方法后,“b”就不會(huì)進(jìn)入字符串常量池腋寨。因?yàn)镾tring a = "b"是Main類的成員變量聪铺,成員變量只有在執(zhí)行到構(gòu)造方法的時(shí)候才會(huì)初始化。
往細(xì)講萄窜,只有執(zhí)行了ldc指令的字符串才會(huì)進(jìn)入字符串常量池
至于ldc
指令的工作原理可以看這篇文章铃剔。
String.intern()
當(dāng)一個(gè)字符串對(duì)象調(diào)用這個(gè)intern方法時(shí),如果該字符串常量池中不包含該對(duì)象引用查刻,也即StringTable不包含該對(duì)象字面量和引用時(shí)键兜,將該字符串對(duì)象引用存入字符串常量中 ,同時(shí)返回該地址穗泵。這樣做的目的是為了提升性能普气,降低開銷,后續(xù)如果定義相同字面量的字符串即可返回該引用(內(nèi)存地址)火欧,不必再在堆上創(chuàng)建字符串實(shí)例棋电。
-
實(shí)例(以下實(shí)例環(huán)境為JDK7以后)
String a="c"; String b = new String("c"); System.out.println("a==b.intern()="+(a==b.intern())); System.out.println("b==b.intern()="+(b==b.intern())); 結(jié)果: a==b.intern()=true b==b.intern()=false
類加載階段,什么都沒干苇侵。
然后運(yùn)行
main
方法赶盔,創(chuàng)建“c”
對(duì)象 ,假設(shè)其地址為0xeee
榆浓,將其加入字符串常量池于未。隨后在堆上創(chuàng)建了String對(duì)象b
,假設(shè)其地址為0xfff
陡鹃。這里
b.intern()
檢測(cè)到了字符串常量池中包含“c”
這個(gè)字符串引用烘浦,所以其返回的是0xeee
,而b
指向的依舊是0xfff
萍鲸,所以第一個(gè)為true
闷叉,第二個(gè)為false
。String a = new String("hellow") + new String("orld"); String b = new String("hello") + new String("world"); System.out.println("a==a.intern()="+(a==a.intern())); System.out.println("a==b.intern()="+(a==b.intern())); System.out.println("b==b.intern()="+(b==b.intern())); 結(jié)果: a==b.intern()=true a==b.intern()=true b==b.intern()=false
類加載階段脊阴,什么都沒干握侧。
然后運(yùn)行
main
方法蚯瞧,創(chuàng)建“hellow”,"orld"
對(duì)象,并放入字符串常量池品擎。然后會(huì)創(chuàng)建一個(gè)"helloworld"
對(duì)象埋合,沒有放入字符串常量池,a
指向這個(gè)"helloworld"
對(duì)象(0xeee
)萄传。接著創(chuàng)建
“hello”,"world"
對(duì)象甚颂,同樣也創(chuàng)建一個(gè)"helloworld"
對(duì)象,也沒有放入字符串常量池秀菱,b
指向這個(gè)"helloworld"
對(duì)象地址(0xfff
)振诬。這時(shí)候第一個(gè)判斷,字符串常量池沒有
“helloworld”
這個(gè)字符串對(duì)象引用答朋,所以將a的引用(0xeee
)放入字符串常量池贷揽,也就是說池子中的引用和a的引用(0xeee
)是一樣的,所以a==a.intern()
梦碗。b.intern()
時(shí)因?yàn)樯弦徊孔址A砍刂幸呀?jīng)有了這個(gè)“helloworld”的引用禽绪,所以他返回回去的引用(0xeee
)就是a
的引用,所以a==b.intern()
洪规。從上面可以清楚的知道
b.intern()
返回的是0xfff印屁,而b
引用地址為0xfff
,所以b!=b.intern()
斩例。// String a1="helloworld"; String a = new String("hello")+new String("world"); System.out.println("a==a=" + (a == a.intern()));
這里的結(jié)果如果
a1
沒有被注釋則為false雄人,注釋了則為true亚隙,原理同上震桶,可以自己腦補(bǔ)一下庇谆。 -
JVM對(duì)字符串的優(yōu)化
String a = "hello"; String b = a+"world"; String c = "helloworld"; String d = "hello"+"world"; System.out.println(b==c); false System.out.println(d==c); true System.out.println(b==d); false Code: stack=3, locals=5, args_size=1 0: ldc #4 // String hello //ldc指令創(chuàng)建字符串對(duì)象“hello” 2: astore_1 // 將a從放入局部變量表(第一個(gè)局部變量映企,第0個(gè)是this) 3: new #5 // class java/lang/StringBuilder //創(chuàng)建StringBuilder對(duì)象 6: dup // 復(fù)制棧頂數(shù)據(jù)(創(chuàng)建StringBuilder對(duì)象)壓入棧中 7: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V 10: aload_1 // 從局部變量中載入a到棧中 11: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; //可以看出字符串相加在字節(jié)碼里就是StringBuilder的append 14: ldc #8 // String world /ldc指令創(chuàng)建字符串對(duì)象“world” 16: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;//繼續(xù)append 19: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; //相加完畢,隱形的調(diào)用toString生成String對(duì)象返回 22: astore_2 // 將b放入局部變量表(第二個(gè)局部變量) 23: ldc #10 // String helloworld //ldc指令創(chuàng)建字符串對(duì)象“helloworld” 25: astore_3 // 將c放入局部變量表(第三個(gè)局部變量) 26: ldc #10 // String helloworld //這里字符串常量池中已經(jīng)包含了helloworld杰捂,就不會(huì)再創(chuàng)建翰灾,直接引用哮肚,而且這個(gè)helloworld是"hello"+"world"拼接的停局,這就是JVM對(duì)字符串的優(yōu)化 28: astore 4 // 將d放入局部變量表(第四個(gè)局部變量) 30: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream; //調(diào)用靜態(tài)方法打印 33: aload_2 // 從局部變量表加載b入棧 34: aload_3 // 從局部變量表加載c入棧 35: if_acmpne 42 // 比較兩個(gè)對(duì)象的引用類型 下面四行就是一個(gè)if else 語句很钓,如果相等就直接doto打印結(jié)果, 38: iconst_1 // 獲得兩個(gè)引用是否相等的結(jié)果(true為1董栽,false為0)码倦,將1入棧 39: goto 43 // 跳轉(zhuǎn)到43行 直接打印出結(jié)果 42: iconst_0 // 兩引用不相等,將0入棧 43: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V 46: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream; 后續(xù)都是相同的意思锭碳,這里就不注釋了袁稽。 49: aload 4 51: aload_3 52: if_acmpne 59 55: iconst_1 56: goto 60 59: iconst_0 60: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V 63: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream; 66: aload_2 67: aload 4 69: if_acmpne 76 72: iconst_1 73: goto 77 76: iconst_0 77: invokevirtual #12 // Method java/io/PrintStream.println:(Z)V 80: return
從上面的字節(jié)碼
可以看出字符串的相加其實(shí)是new了一個(gè)StringBuilder來進(jìn)行append,a和b不相等就是因?yàn)檫@已經(jīng)是兩個(gè)不同的對(duì)象了擒抛,引用也不相等运提。后續(xù)c和d相等是因?yàn)镴VM對(duì)純字符串想加做了調(diào)優(yōu)蝗柔,會(huì)在字節(jié)碼中把他們直接相加后的值賦給局部變量闻葵,所以c和d指向的是同一個(gè)字符串民泵。String a= "a"; for (int i = 0; i < 3; i++) { a+="b"; } Code: stack=2, locals=3, args_size=1 0: ldc #4 // String a 2: astore_1 3: iconst_0 4: istore_2 5: iload_2 6: iconst_3 7: if_icmpge 36 10: new #5 // class java/lang/StringBuilder 13: dup 14: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V 17: aload_1 18: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 21: ldc #2 // String b 23: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 26: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 29: astore_1 30: iinc 2, 1 33: goto 5 36: return
對(duì)于for循環(huán)中的字符串相加(3到33行就是for循環(huán)的內(nèi)容),JVM就沒有優(yōu)化了槽畔,每次相加都是重新創(chuàng)建了StringBuilder栈妆,開銷就是一個(gè)StringBuilder的幾何倍數(shù)那么大,因而在循環(huán)中使用StringBuilder的append來替代直接相加厢钧。
-
總結(jié)
除了日常的如果覺得文章有錯(cuò)誤鳞尔,歡迎指出并交流。這里問一個(gè)問題早直,后續(xù)如果知道了再刪除:字符串常量池和
StringTable
是一個(gè)東西嗎寥假,兩者都是存的字符串引用,但是R大說過StringTable
是存于本地內(nèi)存(native memory
)霞扬,但是看過的文章都說的是字符串常量池位于java堆中糕韧,希望有知道的大佬可以告知一下。 -
引用:
JVM 常量池中存儲(chǔ)的是對(duì)象還是引用呢喻圃?