Java基礎(chǔ)-JVM內(nèi)存管理-常量池與運行時常量池

Java工程師知識樹 / Java基礎(chǔ)


常量池

JVM的常量池主要有以下幾種:

  • class文件常量池
  • 運行時常量池
  • 字符串常量池
  • 基本類型包裝類常量池

相關(guān)之間的關(guān)系為:

圖解說明:

  1. 每個class的字節(jié)碼文件中都有一個常量池耍群,里面是編譯后即知的該class會用到的字面量符號引用,這就是class文件常量池护锤。JVM加載class药版,會將其類信息堂飞,包括class文件常量池置于方法區(qū)中毁涉。
  2. class類信息及其class文件常量池是字節(jié)碼的二進制流她混,它代表的是一個類的靜態(tài)存儲結(jié)構(gòu)拳话,JVM加載類時,需要將其轉(zhuǎn)換為方法區(qū)中的java.lang.Class類的對象實例夕玩;同時你弦,會將class文件常量池中的內(nèi)容導入運行時常量池
  3. 運行時常量池中的常量對應的內(nèi)容只是字面量燎孟,比如一個"字符串"禽作,它還不是String對象;當Java程序在運行時執(zhí)行到這個"字符串"字面量時缤弦,會去字符串常量池里找該字面量的對象引用是否存在领迈,存在則直接返回該引用,不存在則在Java堆里創(chuàng)建該字面量對應的String對象碍沐,并將其引用置于字符串常量池中,然后返回該引用衷蜓。
  4. 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的時候,需要完成以下任務:

  1. 通過該class的全限定名來獲取它的二進制字節(jié)流匀们,即讀取其字節(jié)碼文件缴淋。
  2. 將讀入的字節(jié)流從靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)中的運行時的數(shù)據(jù)結(jié)構(gòu)。
  3. 在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)前提

  1. 實現(xiàn)這種設(shè)計的一個很重要的因素是:String類型是不可變的,實例化后纯露,不可變剿骨,就不會存在多個同樣的字符串實例化后有數(shù)據(jù)沖突;
  2. 運行時埠褪,實例創(chuàng)建的全局字符串常量池中會有一張表浓利,記錄著長相持中每個唯一的字符串對象維護一個引用,當垃圾回收時钞速,發(fā)現(xiàn)該字符串被引用時贷掖,就不會被回收。

實現(xiàn)原理

為了提高性能并減少內(nèi)存的開銷玉工,JVM在實例化字符串常量時進行了一系列的優(yōu)化操作:

  1. 在JVM層面為字符串提供字符串常量池羽资,可以理解為是一個緩存區(qū);
  2. 創(chuàng)建字符串常量時遵班,JVM會檢查字符串常量池中是否存在這個字符串屠升;
  3. 若字符串常量池中存在該字符串,則直接返回引用實例狭郑;若不存在腹暖,先實例化該字符串,并且翰萨,將該字符串放入字符串常量池中脏答,以便于下次使用時,直接取用亩鬼,達到緩存快速使用的效果殖告。

字符串常量池位置變化

方法區(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)存(元空間)?

主要有兩個原因:

  1. 直接內(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操作津滞。
  2. 永久代有一個無法調(diào)整更改的JVM固定大小上限铝侵,回收不完全時,會出現(xiàn)OutOfMemoryError問題触徐;而直接內(nèi)存(元空間)是受到本地機器內(nèi)存的限制咪鲜,不會有這種問題。

總結(jié):

  1. 在JDK1.7前撞鹉,運行時常量池+字符串常量池是存放在方法區(qū)中疟丙,HotSpot VM對方法區(qū)的實現(xiàn)稱為永久代。
  2. 在JDK1.7中鸟雏,字符串常量池從方法區(qū)移到堆中享郊,運行時常量池保留在方法區(qū)中。
  3. 在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]
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末营密,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子目锭,更是在濱河造成了極大的恐慌评汰,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件痢虹,死亡現(xiàn)場離奇詭異被去,居然都是意外死亡,警方通過查閱死者的電腦和手機奖唯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進店門惨缆,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人丰捷,你說我怎么就攤上這事坯墨。” “怎么了病往?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵捣染,是天一觀的道長。 經(jīng)常有香客問我停巷,道長耍攘,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任叠穆,我火速辦了婚禮少漆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘硼被。我一直安慰自己示损,他們只是感情好,可當我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布嚷硫。 她就那樣靜靜地躺著检访,像睡著了一般始鱼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上脆贵,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天医清,我揣著相機與錄音,去河邊找鬼卖氨。 笑死会烙,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的筒捺。 我是一名探鬼主播柏腻,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼系吭!你這毒婦竟也來了五嫂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤肯尺,失蹤者是張志新(化名)和其女友劉穎沃缘,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體则吟,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡槐臀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了逾滥。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片峰档。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖寨昙,靈堂內(nèi)的尸體忽然破棺而出讥巡,到底是詐尸還是另有隱情,我是刑警寧澤舔哪,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布欢顷,位于F島的核電站,受9級特大地震影響捉蚤,放射性物質(zhì)發(fā)生泄漏抬驴。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一缆巧、第九天 我趴在偏房一處隱蔽的房頂上張望布持。 院中可真熱鬧,春花似錦陕悬、人聲如沸题暖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽胧卤。三九已至唯绍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間枝誊,已是汗流浹背况芒。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留叶撒,地道東北人绝骚。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像痊乾,于是被迫代替她去往敵國和親皮壁。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容