一、java虛擬機(jī)內(nèi)存各個區(qū)域總結(jié)
java虛擬機(jī)在運(yùn)行時會把它管理的內(nèi)存分為幾個不同的數(shù)據(jù)區(qū)域康吵,基本上可以分成兩個部分,一個是由所有線程共享的數(shù)據(jù)區(qū)域,另外一個數(shù)據(jù)區(qū)域就是線程自身的數(shù)據(jù)區(qū)域幌陕。其中,共享的數(shù)據(jù)區(qū)域包括方法區(qū)和堆汽煮,線程自身的數(shù)據(jù)區(qū)域有程序計數(shù)器搏熄、虛擬機(jī)棧和本地方法棧棚唆。
程序計數(shù)器
程序計數(shù)器可以理解為當(dāng)前線程執(zhí)行字節(jié)碼的指示器,即可以記錄下一條要執(zhí)行的字節(jié)碼心例,此內(nèi)存區(qū)域是唯一一個在java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError的區(qū)域宵凌。
虛擬機(jī)棧
java虛擬機(jī)棧是用于存儲局部變量表、操作數(shù)棧止后、動態(tài)鏈接摆寄、方法出口等,這里的局部變量表存儲了編譯期可知的各種基本數(shù)據(jù)類型(boolean坯门、byte微饥、char、short古戴、int欠橘、float、long现恼、double)肃续、對象引用類型和returnAddress類型。這里的long和double類型的數(shù)據(jù)占用了2個局部變量空間(Slot)叉袍,其余的數(shù)據(jù)類型只占用1個始锚。在這個區(qū)域,會出現(xiàn)兩種異常情況:一是StackOverFlowError喳逛,這是當(dāng)線程請求的棧深度大于虛擬機(jī)所允許的深度所拋出的異常瞧捌,二是OutOfMemoryError,這是當(dāng)無法申請到足夠的內(nèi)存時會拋出的異常润文。
本地方法棧
本地方法棧和虛擬機(jī)棧是類似的姐呐,區(qū)別在于虛擬機(jī)棧是為虛擬機(jī)執(zhí)行java方法服務(wù)的,本地方法棧是為native方法服務(wù)的典蝌,對于sun hotspot虛擬機(jī)曙砂,就直接把本地方法棧和虛擬機(jī)棧合并到一起。
java堆
在虛擬機(jī)規(guī)范中骏掀,所有的對象實例以及數(shù)組都是在堆上分配的鸠澈,但是現(xiàn)在隨著jit編譯期的發(fā)展以及逃逸分析技術(shù)的成熟,這個規(guī)定也不是絕對的了截驮。java堆是垃圾收集器主要的收集區(qū)域笑陈,也就是我們平常gc的主要收集區(qū)域。
方法區(qū)
方法區(qū)一般用于存儲被虛擬機(jī)加載的類信息侧纯、常量新锈、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)眶熬。如果是HotSpot虛擬機(jī)妹笆,會把方法區(qū)稱為“永生代”,但是像HotSpot這種做法块请,就更容易導(dǎo)致內(nèi)存溢出的問題。在JDK1.7中拳缠,就把原本放在永生代的字符串常量池移出了墩新。
運(yùn)行時常量池
運(yùn)行時常量池是方法區(qū)的一部分,運(yùn)行時常量池對于Class文件常量池的一個重要特征是具備動態(tài)性窟坐,即常量不一定在編譯期就產(chǎn)生海渊,在運(yùn)行期間也可以將新的常量放入池中,例如String類的intern()方法哲鸳。
二臣疑、從jvm角度去理解equals和==的區(qū)別
(1)對于java基本類型(boolean、byte徙菠、char讯沈、short、int婿奔、float缺狠、long、double)萍摊,應(yīng)該使用“==”來比較挤茄,比較的是他們的值。對于復(fù)合類型冰木,使用“==”比較的是它們的在內(nèi)存中的存放地址穷劈,使用equals方法是比較對象在堆內(nèi)存的地址,但在一些諸如String片酝、Integer囚衔、Date類中把Object中的這個方法覆蓋了,作用被覆蓋為比較內(nèi)容是否相同雕沿。由于jdk1.7已經(jīng)把常量池從方法區(qū)移除來了,這里只總結(jié)jdk1.7之后的區(qū)別猴仑。(也會加上jdk6的比較审轮,但是筆者沒有去驗證,僅引用其他人的結(jié)論)
(2)在jdk6辽俗,字符串常量池是在方法區(qū)疾渣,也就是說,對于字符串常量崖飘,是在方法區(qū)分配的內(nèi)存榴捡,而在jdk7之后的版本,是在java堆分配的內(nèi)存朱浴,跟對象實例是在同一個地方分配的內(nèi)存吊圾。還有要記住一點(diǎn)达椰,通過雙引號定義的字符串是直接在字符串常量池產(chǎn)生的,而通過new Stirng()方法產(chǎn)生的字符串是在java堆分配空間的项乒。
(1)利用“==”比較雙引號定義的String類型
/**
* "=="對于復(fù)合類型是比較地址
*/
public void method1(){
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2);
}
結(jié)果:true啰劲,因為s1和s2引用的是同一個字符串,地址是一樣的
(2)利用“equals”比較雙引號定義的String類型
/**
* "equals"本來是比較地址檀何,但是Sting重寫了蝇裤,變成比較值了
*/
public void method2(){
String s1 = "hello";
String s2 = "hello";
System.out.println(s1.equals( s2 ));
}
結(jié)果:true,equals比較的是值频鉴,固兩者是一樣的栓辜。
(3)雙引號定義的字符串和new String()字符串的比較
public void method3(){
String s1 = "hello";
String s2 = new String("hello");
System.out.println(s1 == s2);
System.out.println(s1.equals( s2 ));
}
結(jié)果:false和true,我們重點(diǎn)來看這種情況垛孔。
1啃憎、對于第一句話,是先在棧中創(chuàng)建了一個String類型的對象引用變量s1似炎,然后檢查常量池是否有“hello”這個字符串了辛萍,
如果有,就讓s1指向“hello”羡藐,如果沒有贩毕,就先在常量池創(chuàng)建“hello”字符串,然后讓s1指向“hello”仆嗦。
2辉阶、對于第二句話,也是先在棧中創(chuàng)建了一個String類型的對象引用變量s2瘩扼,然后在堆內(nèi)存中創(chuàng)建一個對象(new string)谆甜,
同時,如果字符串常量池沒有該字符串集绰,也會在常量池生成一個“hello”字符串规辱。
在這里說明一下,對于jdk7栽燕,因為常量池已經(jīng)在堆內(nèi)存了罕袋,所以常量池不一定會放字符串本身,也可能是一個引用碍岔,引用堆內(nèi)存里面的字符串浴讯,
這點(diǎn),在下面的intern()方法會著重說明蔼啦。
上面第三點(diǎn)所分配的內(nèi)存示意圖
image.png
首先榆纽,先在棧創(chuàng)建了一個String類型的對象引用變量s1,然后檢查常量池是否有“hello”這個字符串了,現(xiàn)在沒有奈籽,然后就在常量池創(chuàng)建“hello”饥侵;接著,在棧創(chuàng)建了一個String類型的對象引用變量s2唠摹,在堆內(nèi)存中創(chuàng)建一個對象爆捞,該對象存的就是“hello”字符串,所以s1和s2的地址是不一樣的勾拉,但是值是一樣的煮甥,使用“==”來判斷地址的時候,就會輸出false藕赞,而使用equlas來判斷值的時候成肘,就會輸出true。
三斧蜕、從jvm角度去理解String類的intern()方法双霍。
首先,我們先要知道intern()方法的作用批销,intern()方法的目的在于復(fù)用字符串對象以節(jié)省內(nèi)存洒闸,我們可以簡單地理解為,對一個字符串使用intern()方法的時候均芽,它會去字符串常量池查找該字符串是否已經(jīng)存在丘逸,如果已經(jīng)存在,就會直接返回常量池中的該字符串掀宋。
image.png
我們可以直接看Stirng.intern()方法的api文檔的解釋深纲,上圖紅框部分的意思是,當(dāng)調(diào)用intern()方法的時候劲妙,如果常量池里已經(jīng)包含一個字符串湃鹊,并且該字符串等于調(diào)用intern()方法的字符串對象(兩者是通過equals方法來判斷是否相等的),就返回常量池中的字符串镣奋,否則币呵,就把該字符串對象加入到常量池中,并且返回該字符串對象的引用唆途,因此富雅,對于兩個字符串s和t,s.intern() == t.intern()當(dāng)且僅當(dāng)s.equals(t)為真肛搬。
jdk6的intern()方法詳解
String s = new String("1");
String s2 = "1";
s.intern();
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);
在jdk6中,上面的結(jié)果都是false毕贼,解釋如下:
先看前面四句話:
(1)s在堆內(nèi)存創(chuàng)建了一個“1”對象温赔,然后在字符串常量池也生成了一個“1”;
(2)s2發(fā)現(xiàn)字符串常量池已經(jīng)有“1”了鬼癣,然后就會直接指向“1”陶贼;
(3)s.intern()方法啤贩,會使得s的object對象指向常量池中的“1”
(4)所以,s和s2的地址是不一樣的拜秧,所以第一個輸出false痹屹。
再看后四句話:
(1)s3先在堆內(nèi)存創(chuàng)建一個“1”對象,然后發(fā)現(xiàn)在字符串常量池已經(jīng)有“1”了枉氮,就不會再次生成“1”志衍,此時常量池是沒有“11”字符串的。
(2)s4會在字符串常量池生成了一個“11”聊替,然后讓s4指向“11”
(3)s3.intern()方法楼肪,會使得s3的object對象指向常量池中的“11”
(4)所以,s3和s4的地址是也不一樣的惹悄,所以第二個輸出false春叫。
image.png
如果我們把intern()方法跟前面的代碼調(diào)換,在jdk6中泣港,返回的結(jié)果是一樣的暂殖,都是false,這個就不分析了,這里跟前面不同的一點(diǎn)在于当纱,調(diào)用s3.intern()方法時呛每,由于常量池沒有“11”字符串,就會在常量池創(chuàng)建“11”字符串惫东。
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
結(jié)論:這里無論調(diào)用還是不調(diào)用intern()方法莉给,結(jié)果都是false,因為地址都是不一樣的廉沮,但是在jdk7就不一樣了颓遏。
jdk7的intern()方法詳解
String s = new String("1");
String s2 = "1";
s.intern();
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);
在jdk7中,上面的情況也是返回兩個false滞时。從圖片可以看出來叁幢,跟jdk6的情況基本上是一樣的,沒什么區(qū)別坪稽,這里就不注重解釋曼玩,關(guān)鍵是下面把intern()方法跟上面代碼調(diào)換一行的情況,就大大不同了窒百。
image.png
如果我們把intern()方法跟前面的代碼調(diào)換黍判,在jdk7中,返回的結(jié)果就不一樣了篙梢。
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
此時顷帖,第一個會輸出false,但是,第二個會輸出true,為什么呢贬墩,我們從下圖來看看解釋榴嗅。
第一個輸出跟前面一樣,沒什么區(qū)別陶舞,我們重點(diǎn)看看第二個輸出true的情況:
(1)s3先在堆內(nèi)存創(chuàng)建一個“1”對象嗽测,然后發(fā)現(xiàn)在字符串常量池已經(jīng)有“1”了,就不會再次生成“1”
(2)調(diào)用s3.intern()方法肿孵,如果是jdk6,就會在字符串常量池創(chuàng)建一個“11”字符串了唠粥,但是,對于jdk7,由于常量池就在堆內(nèi)存颁井,此時可以直接使用堆內(nèi)存里已經(jīng)存在的“11”字符串厅贪,就是s3的object對象,也就是說雅宾,字符串常量池不會創(chuàng)建“11”字符串养涮,它只會創(chuàng)建一個引用,該引用就是s3的object對象眉抬。
(3)s4也是直接指向了該引用贯吓,不會在常量池創(chuàng)建“11”字符串。
(4)所以蜀变,s3和s4的地址是一樣的悄谐,所以輸出true。
image.png
結(jié)論:在jdk7中库北,字符串常量池已經(jīng)不需要時刻保存一份字符串了爬舰,相反的,它可以保存一份引用寒瓦,該引用指向堆內(nèi)存的一個字符串對象即可情屹。
對于java內(nèi)存方面的知識就介紹到這里了,筆者對java內(nèi)存的理解不深杂腰,上面所說如果有錯誤垃你,歡迎指出。
參考資料
https://www.cnblogs.com/fengbs/p/7029013.html
http://blog.csdn.net/seu_calvin/article/details/52089040
http://blog.csdn.net/seu_calvin/article/details/52291082