這篇文章主要是做一個(gè)總結(jié),將查找到相關(guān)的資料自己做一個(gè)整理,最后會(huì)列出查找過(guò)的相關(guān)資料,感興趣的可以去翻一翻。
常量池
-
class文件常量池(class constant pool)
常量池可以理解為Class文件之中的資源倉(cāng)庫(kù),它是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語(yǔ)言層面的常量概念,如文本字符串、聲明為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)
可以通過(guò)查看字節(jié)碼的形式來(lái)查看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)存中開(kā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è)。在解析過(guò)程中需要將常量池中所有的符號(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)過(guò)驗(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ù)上面說(shuō)的,在類加載階段,這個(gè) resolve 階段( constant pool resolution )是lazy的。換句話說(shuō)并沒(méi)有真正的對(duì)象,字符串常量池里自然也沒(méi)有,那么ldc指令還怎么把人推送至棧頂?或者換一個(gè)角度想,既然resolve 階段是lazy的,那總有一個(gè)時(shí)候它要真正的執(zhí)行吧,是什么時(shí)候?執(zhí)行l(wèi)dc指令就是觸發(fā)這個(gè)lazy resolution動(dòng)作的條件。
ldc字節(jié)碼在這里的執(zhí)行語(yǔ)義是:到當(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的過(guò)程如果發(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è)引用出去??梢?jiàn),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í)返回該地址。這樣做的目的是為了提升性能,降低開(kāi)銷,后續(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類加載階段,什么都沒(méi)干。
然后運(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類加載階段,什么都沒(méi)干。
然后運(yùn)行
main方法,創(chuàng)建“hellow”,"orld"對(duì)象,并放入字符串常量池。然后會(huì)創(chuàng)建一個(gè)"helloworld"對(duì)象,沒(méi)有放入字符串常量池,a指向這個(gè)"helloworld"對(duì)象(0xeee)。接著創(chuàng)建
“hello”,"world"對(duì)象,同樣也創(chuàng)建一個(gè)"helloworld"對(duì)象,也沒(méi)有放入字符串常量池,b指向這個(gè)"helloworld"對(duì)象地址(0xfff)。這時(shí)候第一個(gè)判斷,字符串常量池沒(méi)有
“helloworld”這個(gè)字符串對(duì)象引用,所以將a的引用(0xeee)放入字符串常量池,也就是說(shuō)池子中的引用和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沒(méi)有被注釋則為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 語(yǔ)句,如果相等就直接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來(lái)進(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就沒(méi)有優(yōu)化了,每次相加都是重新創(chuàng)建了StringBuilder,開(kāi)銷就是一個(gè)StringBuilder的幾何倍數(shù)那么大,因而在循環(huán)中使用StringBuilder的append來(lái)替代直接相加。
-
總結(jié)
除了日常的如果覺(jué)得文章有錯(cuò)誤,歡迎指出并交流。這里問(wèn)一個(gè)問(wèn)題,后續(xù)如果知道了再刪除:字符串常量池和
StringTable是一個(gè)東西嗎,兩者都是存的字符串引用,但是R大說(shuō)過(guò)StringTable是存于本地內(nèi)存(native memory),但是看過(guò)的文章都說(shuō)的是字符串常量池位于java堆中,希望有知道的大佬可以告知一下。 -
引用:
JVM 常量池中存儲(chǔ)的是對(duì)象還是引用呢?