Java常量池

jvm虛擬內(nèi)存分布:

jvm虛擬內(nèi)存分布

程序計(jì)數(shù)器是jvm執(zhí)行程序的流水線,存放一些跳轉(zhuǎn)指令沉噩。

本地方法棧是jvm調(diào)用操作系統(tǒng)方法所使用的棧捺宗。

虛擬機(jī)棧是jvm執(zhí)行java代碼所使用的棧。

方法區(qū)存放了一些常量川蒙、靜態(tài)變量蚜厉、類信息等,可以理解成class文件在內(nèi)存中的存放位置派歌。

虛擬機(jī)堆是jvm執(zhí)行java代碼所使用的堆弯囊。

常量池

Java中的常量池,實(shí)際上分為兩種形態(tài):靜態(tài)常量池運(yùn)行時常量池胶果。

  • 靜態(tài)常量池

所謂靜態(tài)常量池匾嘱,即.class文件中的常量池,class文件中的常量池不僅僅包含字符串(數(shù)字)字面量早抠,還包含類霎烙、方法的信息,占用class文件絕大部分空間。這種常量池主要用于存放兩大類常量:字面量(Literal)和符號引用量*(Symbolic References)悬垃,字面量相當(dāng)于Java語言層面常量的概念游昼,如文本字符串,聲明為final的常量值等尝蠕,符號引用則屬于編譯原理方面的概念烘豌,包括了如下三種類型的常量:

類和接口的全限定名
字段名稱和描述符
方法名稱和描述符

  • 運(yùn)行時常量池

運(yùn)行時常量池,則是jvm虛擬機(jī)在完成類裝載操作后看彼,將class文件中的常量池載入到內(nèi)存中廊佩,并保存在方法區(qū)中,我們常說的常量池靖榕,就是指方法區(qū)中的運(yùn)行時常量池标锄。

運(yùn)行時常量池相對于CLass文件常量池的另外一個重要特征是具備動態(tài)性,Java語言并不要求常量一定只有編譯期才能產(chǎn)生茁计,也就是并非預(yù)置入CLass文件中常量池的內(nèi)容才能進(jìn)入方法區(qū)運(yùn)行時常量池料皇,運(yùn)行期間也可能將新的常量放入池中,這種特性被開發(fā)人員利用比較多的就是String類的intern()方法星压。

String的intern()方法會查找在常量池中是否存在一份equal相等的字符串,如果有則返回該字符串的引用,如果沒有則添加自己的字符串進(jìn)入常量池践剂。

常量池的好處

常量池是為了避免頻繁的創(chuàng)建和銷毀對象而影響系統(tǒng)性能,其實(shí)現(xiàn)了對象的共享租幕。
例如字符串常量池舷手,在編譯階段就把所有的字符串文字放到一個常量池中。
(1)節(jié)省內(nèi)存空間:常量池中所有相同的字符串常量被合并劲绪,只占用一個空間。
(2)節(jié)省運(yùn)行時間:比較字符串時盆赤,==比equals()快贾富。對于兩個引用變量,只用==判斷引用是否相等牺六,也就可以判斷實(shí)際值是否相等颤枪。

栗子

  • 栗子1
String s1 = "Hello";
String s2 = "Hello";
String s3 = "Hel" + "lo";
String s4 = "Hel" + new String("lo");
String s5 = new String("Hello");
String s6 = s5.intern();
String s7 = "H";
String s8 = "ello";
String s9 = s7 + s8;
         
System.out.println(s1 == s2);  // true
System.out.println(s1 == s3);  // true
System.out.println(s1 == s4);  // false
System.out.println(s1 == s9);  // false
System.out.println(s4 == s5);  // false
System.out.println(s1 == s6);  // true

首先說明一點(diǎn),在java 中淑际,直接使用==操作符畏纲,比較的是兩個字符串的引用地址撵彻,并不是比較內(nèi)容起胰,比較內(nèi)容請用String.equals()箱季。

s1 == s2這個非常好理解牧挣,s1总滩、s2在賦值時男摧,均使用的字符串字面量索绪,說白話點(diǎn)契吉,就是直接把字符串寫死,在編譯期間屑迂,這種字面量會直接放入class文件的常量池中浸策,從而實(shí)現(xiàn)復(fù)用,載入運(yùn)行時常量池后惹盼,s1庸汗、s2指向的是同一個內(nèi)存地址,所以相等手报。

s1 == s3這個地方有個坑夫晌,s3雖然是動態(tài)拼接出來的字符串,但是所有參與拼接的部分都是已知的字面量昧诱,在編譯期間晓淀,這種拼接會被優(yōu)化,編譯器直接幫你拼好盏档,因此String s3 = "Hel" + "lo";在class文件中被優(yōu)化成String s3 = "Hello"凶掰,所以s1 == s3成立。只有使用引號包含文本的方式創(chuàng)建的String對象之間使用“+”連接產(chǎn)生的新對象才會被加入字符串池中蜈亩。

s1 == s4當(dāng)然不相等懦窘,s4雖然也是拼接出來的,但new String("lo")這部分不是已知字面量稚配,是一個不可預(yù)料的部分畅涂,編譯器不會優(yōu)化,必須等到運(yùn)行時才可以確定結(jié)果道川,結(jié)合字符串不變定理午衰,鬼知道s4被分配到哪去了,所以地址肯定不同冒萄。對于所有包含new方式新建對象(包括null)的“+”連接表達(dá)式臊岸,它所產(chǎn)生的新對象都不會被加入字符串池中。
配上一張簡圖理清思路:

java字符串不變

s1 == s9也不相等尊流,道理差不多帅戒,雖然s7、s8在賦值的時候使用的字符串字面量崖技,但是拼接成s9的時候逻住,s7、s8作為兩個變量迎献,都是不可預(yù)料的瞎访,編譯器畢竟是編譯器,不可能當(dāng)解釋器用忿晕,不能在編譯期被確定装诡,所以不做優(yōu)化银受,只能等到運(yùn)行時,在堆中創(chuàng)建s7鸦采、s8拼接成的新字符串宾巍,在堆中地址不確定,不可能與方法區(qū)常量池中的s1地址相同渔伯。


jvm常量池顶霞,堆,棧內(nèi)存分布

s4 == s5已經(jīng)不用解釋了锣吼,絕對不相等选浑,二者都在堆中,但地址不同玄叠。

s1 == s6這兩個相等完全歸功于intern方法古徒,s5在堆中,內(nèi)容為Hello 读恃,intern方法會嘗試將Hello字符串添加到常量池中隧膘,并返回其在常量池中的地址,因?yàn)槌A砍刂幸呀?jīng)有了Hello字符串寺惫,所以intern方法直接返回地址疹吃;而s1在編譯期就已經(jīng)指向常量池了,因此s1和s6指向同一地址西雀,相等萨驶。

  • 栗子2
public static final String A = "ab"; // 常量A
public static final String B = "cd"; // 常量B
public static void main(String[] args) {
     String s = A + B;  // 將兩個常量用+連接對s進(jìn)行初始化 
     String t = "abcd";   
    if (s == t) {   
         System.out.println("s等于t,它們是同一個對象");   
     } else {   
         System.out.println("s不等于t艇肴,它們不是同一個對象");   
     }   
 } 

s等于t腔呜,它們是同一個對象

A和B都是常量,值是固定的豆挽,因此s的值也是固定的育谬,它在類被編譯時就已經(jīng)確定了。也就是說:String s=A+B; 等同于:String s="ab"+"cd";

  • 栗子3
public static final String A; // 常量A
public static final String B;    // 常量B
static {   
     A = "ab";   
     B = "cd";   
 }   
 public static void main(String[] args) {   
    // 將兩個常量用+連接對s進(jìn)行初始化   
     String s = A + B;   
     String t = "abcd";   
    if (s == t) {   
         System.out.println("s等于t帮哈,它們是同一個對象");   
     } else {   
         System.out.println("s不等于t,它們不是同一個對象");   
     }   
 } 

s不等于t锰镀,它們不是同一個對象

A和B雖然被定義為常量娘侍,但是它們都沒有馬上被賦值。在運(yùn)算出s的值之前泳炉,他們何時被賦值憾筏,以及被賦予什么樣的值,都是個變數(shù)花鹅。因此A和B在被賦值之前氧腰,性質(zhì)類似于一個變量。那么s就不能在編譯期被確定,而只能在運(yùn)行時被創(chuàng)建了古拴。

至此箩帚,我們可以得出三個非常重要的結(jié)論:

  • 必須要關(guān)注編譯期的行為,才能更好的理解常量池黄痪。

  • 運(yùn)行時常量池中的常量紧帕,基本來源于各個class文件中的常量池。

  • 程序運(yùn)行時桅打,除非手動向常量池中添加常量(比如調(diào)用intern方法)是嗜,否則jvm不會自動添加常量到常量池。

以上所講僅涉及字符串常量池挺尾,實(shí)際上還有整型常量池鹅搪、浮點(diǎn)型常量池(java中基本類型的包裝類的大部分都實(shí)現(xiàn)了常量池技術(shù),即Byte,Short,Integer,Long,Character,Boolean遭铺;兩種浮點(diǎn)數(shù)類型的包裝類Float,Double并沒有實(shí)現(xiàn)常量池技術(shù)) 等等丽柿,但都大同小異,只不過數(shù)值類型的常量池不可以手動添加常量掂僵,程序啟動時常量池中的常量就已經(jīng)確定了航厚,比如整型常量池中的常量范圍:-128~127,(Byte,Short,Integer,Long,Character,Boolean)這5種包裝類默認(rèn)創(chuàng)建了數(shù)值[-128锰蓬,127]的相應(yīng)類型的緩存數(shù)據(jù)幔睬,但是超出此范圍仍然會去創(chuàng)建新的對象。

例如在自動裝箱時芹扭,把int變成Integer的時候麻顶,是有規(guī)則的,當(dāng)你的int的值在-128-IntegerCache.high(127) 時舱卡,返回的不是一個新new出來的Integer對象辅肾,而是一個已經(jīng)緩存在堆 中的Integer對象,(我們可以這樣理解轮锥,系統(tǒng)已經(jīng)把-128到127之 間的Integer緩存到一個Integer數(shù)組中去了矫钓,如果你要把一個int變成一個Integer對象,首先去緩存中找舍杜,找到的話直接返回引用給你就 行了新娜,不必再新new一個),如果不在-128-IntegerCache.high(127) 時會返回一個新new出來的Integer對象既绩。

深入字節(jié)碼

前文提到過概龄,class文件中存在一個靜態(tài)常量池,這個常量池是由編譯器生成的饲握,用來存儲java源文件中的字面量(本文僅僅關(guān)注字面量)私杜,假設(shè)我們有如下java代碼:

public class HelloWorld{
  public static void main(String args[]){
    System.out.println("hello world");
  }
}

為了方便起見蚕键,就這么簡單,沒錯衰粹!將代碼編譯成class文件后锣光,用winhex打開二進(jìn)制格式的class文件。如圖:

class文件

class文件的結(jié)構(gòu)

(1)魔數(shù)
開頭的4個字節(jié)是class文件魔數(shù)寄猩,用來標(biāo)識這是一個class文件嫉晶,說白話點(diǎn)就是文件頭,確定一個文件是否能被JVM接受田篇,既:CA FE BA BE替废。

(2)版本號
第5和第6個字節(jié)是次版本號,第7個和第8 個是主版本號泊柬。這里的第7和第8位是0034椎镣,即:0x0034。0x0034轉(zhuǎn)為10進(jìn)制是52兽赁。Java的版本是從45開始的然而從1.0 到1.1 是45.0到45.3, 之后就是1.2 對應(yīng)46状答, 1.3 對應(yīng)47 … 1.6 對應(yīng)50,我這里是1.6.0_24對應(yīng)的是52,就是0x0034;

(3)常量池的入口
由于常量池中的常量的數(shù)量不是固定的刀崖,所以常量池的入口需要放置一項(xiàng)u2類型的數(shù)據(jù)惊科,代表常量池的容量計(jì)數(shù)值。這里的常量池容量計(jì)數(shù)值是從1開始的亮钦。如圖常量池的容量:0x001d(29)馆截。所以共有29個常量。

(4)常量池
常量池中主要存放兩類常量:字面量和符號引用蜂莉。字面量比較接近Java語言層面的常量概念蜡娶。就是我們什么提到的常量。而符號引用則屬于編譯原理的方面的概念映穗。包括以下三類常量:

類和接口的全限定名
字段的名稱和描述符
方法的名稱和描述符

class文件就先介紹到這里窖张。

接下來再說說運(yùn)行時常量池,由于運(yùn)行時常量池在方法區(qū)中蚁滋,我們可以通過jvm參數(shù):-XX:PermSize宿接、-XX:MaxPermSize來設(shè)置方法區(qū)大小,從而間接限制常量池大小辕录。

假設(shè)jvm啟動參數(shù)為:-XX:PermSize=2M -XX:MaxPermSize=2M澄阳,然后運(yùn)行如下代碼:

1 //保持引用,防止自動垃圾回收
List<String> list = new ArrayList<String>();
        
int i = 0;
       
while(true){
  //通過intern方法向常量池中手動添加常量
  list.add(String.valueOf(i++).intern());
}

程序立刻會拋出:Exception in thread "main" java.lang.outOfMemoryError: PermGen space異常踏拜。PermGen space正是方法區(qū),足以說明常量池在方法區(qū)中低剔。

在jdk8中速梗,移除了方法區(qū)肮塞,轉(zhuǎn)而用Metaspace區(qū)域替代,所以我們需要使用新的jvm參數(shù):-XX:MaxMetaspaceSize=2M姻锁,依然運(yùn)行如上代碼枕赵,拋出:java.lang.OutOfMemoryError: Metaspace異常。同理說明運(yùn)行時常量池是劃分在Metaspace區(qū)域中位隶。具體關(guān)于Metaspace區(qū)域的知識拷窜,請自行搜索。

原文參考:
深入淺出java常量池
Java常量池理解和經(jīng)典總結(jié)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末涧黄,一起剝皮案震驚了整個濱河市篮昧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌笋妥,老刑警劉巖懊昨,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異春宣,居然都是意外死亡酵颁,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進(jìn)店門月帝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來躏惋,“玉大人,你說我怎么就攤上這事嚷辅〔疽蹋” “怎么了?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵潦蝇,是天一觀的道長款熬。 經(jīng)常有香客問我,道長攘乒,這世上最難降的妖魔是什么贤牛? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮则酝,結(jié)果婚禮上殉簸,老公的妹妹穿的比我還像新娘。我一直安慰自己沽讹,他們只是感情好般卑,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著爽雄,像睡著了一般蝠检。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上挚瘟,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天叹谁,我揣著相機(jī)與錄音饲梭,去河邊找鬼。 笑死焰檩,一個胖子當(dāng)著我的面吹牛憔涉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播析苫,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼兜叨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了衩侥?” 一聲冷哼從身側(cè)響起国旷,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎顿乒,沒想到半個月后议街,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡璧榄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年特漩,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片骨杂。...
    茶點(diǎn)故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡涂身,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出搓蚪,到底是詐尸還是另有隱情蛤售,我是刑警寧澤,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布妒潭,位于F島的核電站悴能,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏雳灾。R本人自食惡果不足惜漠酿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望谎亩。 院中可真熱鬧炒嘲,春花似錦、人聲如沸匈庭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽阱持。三九已至夭拌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背啼止。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工道逗, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人献烦。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像卖词,于是被迫代替她去往敵國和親巩那。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,658評論 2 350

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

  • 一此蜈、概述 常量池:編譯期被確定即横,*.class文件中的一部分,包含字面量(Literal)和符號引用(Symbol...
    高稷閱讀 1,911評論 0 2
  • 相關(guān)概念 常量池的定義常量池(constant pool):指的是在編譯期被確定裆赵,并被保存在已編譯的.class文...
    snoweek閱讀 793評論 0 4
  • 一.相關(guān)概念 什么是常量用final修飾的成員變量表示常量东囚,值一旦給定就無法改變!final修飾的變量有三種:靜態(tài)...
    夢工廠閱讀 58,047評論 38 277
  • java常量池是一個經(jīng)久不衰的話題战授,也是面試官的最愛页藻,題目花樣百出。理論jvm虛擬內(nèi)存分布: ** 程序計(jì)...
    Java紅茶閱讀 338評論 0 4
  • java常量池是一個經(jīng)久不衰的話題植兰,也是面試官的最愛份帐,題目花樣百出,小菜早就對常量池有所耳聞楣导,這次好好總結(jié)一下废境。 ...
    堤岸小跑閱讀 307評論 0 0