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)生的新對象都不會被加入字符串池中。
配上一張簡圖理清思路:
s1 == s9也不相等尊流,道理差不多帅戒,雖然s7、s8在賦值的時候使用的字符串字面量崖技,但是拼接成s9的時候逻住,s7、s8作為兩個變量迎献,都是不可預(yù)料的瞎访,編譯器畢竟是編譯器,不可能當(dāng)解釋器用忿晕,不能在編譯期被確定装诡,所以不做優(yōu)化银受,只能等到運(yùn)行時,在堆中創(chuàng)建s7鸦采、s8拼接成的新字符串宾巍,在堆中地址不確定,不可能與方法區(qū)常量池中的s1地址相同渔伯。
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文件的結(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ū)域的知識拷窜,請自行搜索。