常量池
JVM的常量池主要有以下幾種:
- class文件常量池
- 運行時常量池
- 字符串常量池
- 基本類型包裝類常量池
相關(guān)之間的關(guān)系為:
圖解說明:
- 每個class的字節(jié)碼文件中都有一個常量池耍群,里面是編譯后即知的該class會用到的
字面量
與符號引用
,這就是class文件常量池
护锤。JVM加載class药版,會將其類信息堂飞,包括class文件常量池置于方法區(qū)中毁涉。 - class類信息及其class文件常量池是字節(jié)碼的二進制流她混,它代表的是一個類的靜態(tài)存儲結(jié)構(gòu)拳话,JVM加載類時,需要將其轉(zhuǎn)換為方法區(qū)中的
java.lang.Class
類的對象實例夕玩;同時你弦,會將class文件常量池中的內(nèi)容導入運行時常量池
。 - 運行時常量池中的常量對應的內(nèi)容只是字面量燎孟,比如一個"字符串"禽作,它還不是String對象;當Java程序在運行時執(zhí)行到這個"字符串"字面量時缤弦,會去
字符串常量池
里找該字面量的對象引用是否存在领迈,存在則直接返回該引用,不存在則在Java堆里創(chuàng)建該字面量對應的String對象碍沐,并將其引用置于字符串常量池中,然后返回該引用衷蜓。 - Java的基本數(shù)據(jù)類型中累提,除了兩個浮點數(shù)類型,其他的基本數(shù)據(jù)類型都在各自內(nèi)部實現(xiàn)了常量池磁浇,但都在[-128~127]這個范圍內(nèi)斋陪。
class文件常量池
測試代碼:
public class Test2{
public static void main(String[] args) {
int ct = 0;
for (int i = 0; i < 100; i++) {
ct++;
}
System.out.println("ct:"+ct);
}
public void test(){
String str = "test";
System.out.println(str);
}
}
使用反編譯命令:javap -verbose Test2.class
public class Test2
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool: // 以下就是class文件常量池 使用#加數(shù)字標記每個“常量”。
#1 = Methodref #12.#23 // java/lang/Object."<init>":()V
#2 = Fieldref #24.#25 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Class #26 // java/lang/StringBuilder
#4 = Methodref #3.#23 // java/lang/StringBuilder."<init>":()V
#5 = String #27 // ct:
#6 = Methodref #3.#28 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#7 = Methodref #3.#29 // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
#8 = Methodref #3.#30 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Methodref #31.#32 // java/io/PrintStream.println:(Ljava/lang/String;)V
#10 = String #33 // zifuchuan
#11 = Class #34 // Test2
#12 = Class #35 // java/lang/Object
#13 = Utf8 <init>
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 main
#18 = Utf8 ([Ljava/lang/String;)V
#19 = Utf8 StackMapTable
#20 = Utf8 testT
#21 = Utf8 SourceFile
#22 = Utf8 Test2.java
#23 = NameAndType #13:#14 // "<init>":()V
#24 = Class #36 // java/lang/System
#25 = NameAndType #37:#38 // out:Ljava/io/PrintStream;
#26 = Utf8 java/lang/StringBuilder
#27 = Utf8 ct:
#28 = NameAndType #39:#40 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#29 = NameAndType #39:#41 // append:(I)Ljava/lang/StringBuilder;
#30 = NameAndType #42:#43 // toString:()Ljava/lang/String;
#31 = Class #44 // java/io/PrintStream
#32 = NameAndType #45:#46 // println:(Ljava/lang/String;)V
#33 = Utf8 zifuchuan
#34 = Utf8 Test2
#35 = Utf8 java/lang/Object
#36 = Utf8 java/lang/System
#37 = Utf8 out
#38 = Utf8 Ljava/io/PrintStream;
#39 = Utf8 append
#40 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#41 = Utf8 (I)Ljava/lang/StringBuilder;
#42 = Utf8 toString
#43 = Utf8 ()Ljava/lang/String;
#44 = Utf8 java/io/PrintStream
#45 = Utf8 println
#46 = Utf8 (Ljava/lang/String;)V
{...}
class文件常量池存放的是該class編譯后即知的,在運行時將會用到的各個“常量”无虚。
注意這個常量不是編程中所說的final
修飾的變量缔赠,而是字面量
和符號引用
,如下圖所示:
字面量
字面量大約相當于Java代碼中的雙引號字符串和常量的實際的值友题,包括:
1.文本字符串嗤堰,即代碼中用雙引號包裹的字符串部分的值。
例如剛剛的例子中度宦,有三個字符串:"zifuchuan"
踢匣,"ct:"
,它們在class文件常量池中分別對應:
#33 = Utf8 zifuchuan
#27 = Utf8 ct:
這里的
#49
就是"張三"
的字面量戈抄,它不是一個String對象离唬,只是一個使用utf8編碼的文本字符串而已。
2.用final修飾的成員變量划鸽,例如输莺,private static final int entranceAge = 18;
這條語句定義了一個final常量entranceAge
,它的值是18
裸诽,對應在class文件常量池中就會有:#25 = Integer 18
#25 = Integer 18
注意模闲,只有final修飾的成員變量如
entranceAge
,才會在常量池中存在對應的字面量崭捍。而非final的成員變量scores
尸折,以及局部變量base
(即使使用final修飾了),它們的字面量都不會在常量池中定義殷蛇。
符號引用
符號引用包括
1.類和接口的全限定名实夹,例如:
#3 = Class #26 // java/lang/StringBuilder
#26 = Utf8 java/lang/StringBuilder
2.方法的名稱和描述符,例如:
#20 = Utf8 testT
以及這種對其他類的方法的引用:
#9 = Methodref #31.#32 // java/io/PrintStream.println:(Ljava/lang/String;)V
#31 = Class #44 // java/io/PrintStream
#32 = NameAndType #45:#46 // println:(Ljava/lang/String;)V
#44 = Utf8 java/io/PrintStream
#45 = Utf8 println
#46 = Utf8 (Ljava/lang/String;)V
3.字段的名稱和描述符粒梦,例如:
#2 = Fieldref #24.#25 // java/lang/System.out:Ljava/io/PrintStream;
#24 = Class #36 // java/lang/System
#25 = NameAndType #37:#38 // out:Ljava/io/PrintStream;
#36 = Utf8 java/lang/System
#37 = Utf8 out
#38 = Utf8 Ljava/io/PrintStream;
以及這種局部變量:
#33 = Utf8 zifuchuan
運行時常量池
運行時常量池包括導入class文件常量池的內(nèi)容和符號引用對應的直接引用(實際內(nèi)存地址)亮航。
JVM在加載某個class的時候,需要完成以下任務:
- 通過該class的全限定名來獲取它的二進制字節(jié)流匀们,即讀取其字節(jié)碼文件缴淋。
- 將讀入的字節(jié)流從靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)中的運行時的數(shù)據(jù)結(jié)構(gòu)。
- 在Java堆中生成該class對應的類對象泄朴,代表該class原信息重抖。這個類對象的類型是
java.lang.Class
,它與普通對象不同的地方在于祖灰,普通對象一般都是在new之后創(chuàng)建的钟沛,而類對象是在類加載的時候創(chuàng)建的,且是單例局扶。
而上述過程的第二步恨统,就包含了將class文件常量池內(nèi)容導入運行時常量池叁扫。class文件常量池是一個class文件對應一個常量池,而運行時常量池只有一個畜埋,多個class文件常量池中的相同字符串只會對應運行時常量池中的一個字符串莫绣。
運行時常量池除了導入class文件常量池的內(nèi)容,還會保存符號引用對應的直接引用(實際內(nèi)存地址)悠鞍。這些直接引用是JVM在類加載之后的鏈接(驗證对室、準備、解析)階段從符號引用翻譯過來的狞玛。
此外软驰,運行時常量池具有動態(tài)性的特征,它的內(nèi)容并不是全部來源與編譯后的class文件心肪,在運行時也可以通過代碼生成常量并放入運行時常量池锭亏。
要注意的是,運行時常量池中保存的“常量”依然是字面量
和符號引用
硬鞍。比如字符串慧瘤,這里放的仍然是單純的文本字符串,而不是String對象固该。
字符串常量池
字符串常量池由來
在日常開發(fā)過程中锅减,字符串的創(chuàng)建是比較頻繁的,而字符串的分配和其他對象的分配是類似的伐坏,需要耗費大量的時間和空間怔匣,從而影響程序的運行性能,所以作為最基礎(chǔ)最常用的引用數(shù)據(jù)類型桦沉,Java設(shè)計者在JVM層面提供了字符串常量池每瞒。
實現(xiàn)前提
- 實現(xiàn)這種設(shè)計的一個很重要的因素是:String類型是不可變的,實例化后纯露,不可變剿骨,就不會存在多個同樣的字符串實例化后有數(shù)據(jù)沖突;
- 運行時埠褪,實例創(chuàng)建的全局字符串常量池中會有一張表浓利,記錄著長相持中每個唯一的字符串對象維護一個引用,當垃圾回收時钞速,發(fā)現(xiàn)該字符串被引用時贷掖,就不會被回收。
實現(xiàn)原理
為了提高性能并減少內(nèi)存的開銷玉工,JVM在實例化字符串常量時進行了一系列的優(yōu)化操作:
- 在JVM層面為字符串提供字符串常量池羽资,可以理解為是一個緩存區(qū);
- 創(chuàng)建字符串常量時遵班,JVM會檢查字符串常量池中是否存在這個字符串屠升;
- 若字符串常量池中存在該字符串,則直接返回引用實例狭郑;若不存在腹暖,先實例化該字符串,并且翰萨,將該字符串放入字符串常量池中脏答,以便于下次使用時,直接取用亩鬼,達到緩存快速使用的效果殖告。
字符串常量池位置變化
方法區(qū)
提到字符串常量池,還得先從方法區(qū)說起雳锋。方法區(qū)和Java堆一樣(但是方法區(qū)是非堆
)黄绩,是各個線程共享的內(nèi)存區(qū)域,是用于存儲已經(jīng)被JVM加載的類信息玷过、常量爽丹、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)辛蚊。
很多人會把方法區(qū)稱為永久代
粤蝎,其實本質(zhì)上是不等價的,只不過HotSpot虛擬機設(shè)計團隊是選擇把GC分代收集擴展到了方法區(qū)袋马,使用永久代來代替實現(xiàn)方法區(qū)初澎。其實,在方法區(qū)中的垃圾收集行為還是比較少的虑凛,這個區(qū)域的內(nèi)存回收目標主要是針對常量池的回收和對類型的卸載碑宴,但是這個區(qū)域的回收總是不盡如人意的,如果該區(qū)域回收不完全就會出現(xiàn)內(nèi)存泄露卧檐。當然墓懂,對于JDK1.8
時,HostSpot VM對JVM模型進行了改造霉囚,將元數(shù)據(jù)放到本地內(nèi)存
捕仔,將常量池和靜態(tài)變量放到了Java堆
里。
元空間
JDK 1.8, HotSpot JVM將永久代移除了盈罐,使用本地內(nèi)存來存儲類的元數(shù)據(jù)信息榜跌,即為元空間(Metaspace)
所以,字符串常量池的具體位置是在哪里盅粪?當然這個我們后面需要區(qū)分jdk的版本钓葫,jdk1.7之前,jdk1.7票顾,以及jdk1.8础浮,因為這些版本中帆调,字符串常量池因為方法區(qū)的改變而做了一些變化。
JDK1.7之前
在jdk1.7之前豆同,常量池是存放在方法區(qū)中的番刊。
JDK1.7
在jdk1.7中,字符串常量池移到了堆中影锈,運行時常量池還在方法區(qū)中芹务。
JDK1.8
jdk1.8刪除了永久代,方法區(qū)這個概念還是保留的鸭廷,但是方法區(qū)的實現(xiàn)變成了元空間
枣抱,常量池沿用jdk1.7,還是放在了堆中辆床。這樣的效果就變成了:常量池與靜態(tài)變量存儲到了堆中佳晶,類的元數(shù)據(jù)及運行時常量池存儲到元空間中。
為啥要把方法區(qū)從JVM內(nèi)存(永久代)移到直接內(nèi)存(元空間)?
主要有兩個原因:
- 直接內(nèi)存屬于本地系統(tǒng)的IO操作,具有更高的一個IO操作性能舟奠,而JVM的堆內(nèi)存這種呆馁,如果有IO操作,也是先復制到直接內(nèi)存,然后再去進行本地IO操作。經(jīng)過了一系列的中間流程,性能就會差一些逸贾。非直接內(nèi)存操作:
本地IO操作——>直接內(nèi)存操作——>非直接內(nèi)存操作——>直接內(nèi)存操作——>本地IO操作
,而直接內(nèi)存操作:本地IO操作——>直接內(nèi)存操作——>本地IO操作
津滞。 - 永久代有一個無法調(diào)整更改的JVM固定大小上限铝侵,回收不完全時,會出現(xiàn)
OutOfMemoryError
問題触徐;而直接內(nèi)存(元空間)是受到本地機器內(nèi)存的限制咪鲜,不會有這種問題。
總結(jié):
- 在JDK1.7前撞鹉,運行時常量池+字符串常量池是存放在方法區(qū)中疟丙,HotSpot VM對方法區(qū)的實現(xiàn)稱為永久代。
- 在JDK1.7中鸟雏,字符串常量池從方法區(qū)移到堆中享郊,運行時常量池保留在方法區(qū)中。
- 在JDK1.8中孝鹊,HotSpot移除永久代炊琉,使用元空間代替,此時字符串常量池保留在堆中又活,運行時常量池保留在方法區(qū)中苔咪,只是實現(xiàn)不一樣了锰悼,JVM內(nèi)存變成了直接內(nèi)存。
基本類型包裝類常量池
除了字符串常量池悼泌,Java的基本類型的封裝類大部分也都實現(xiàn)了常量池松捉。包括Byte,Short,Integer,Long,Character,Boolean
夹界,注意馆里,浮點數(shù)據(jù)類型Float,Double
是沒有常量池的。
封裝類的常量池是在各自內(nèi)部類中實現(xiàn)的可柿,比如IntegerCache
(Integer
的內(nèi)部類)鸠踪,自然也位于堆區(qū)。
要注意的是复斥,這些常量池是有范圍的:
- Byte,Short,Integer,Long : [-128~127]
- Character : [0~127]
- Boolean : [True, False]