任何語言所編寫的程序,其中的各類型的數(shù)據(jù)都需要一個存儲位置,Java中數(shù)據(jù)的存儲位置分為以下5種:
1.寄存器
最快的存儲區(qū),位于處理器內(nèi)部抚恒,但是數(shù)量極其有限。所以寄存器根據(jù)需求進行自動分配络拌,無法直接人為控制俭驮。
2.棧內(nèi)存
位于RAM當中,通過堆棧指針可以從處理器獲得直接支持。堆棧指針向下移動混萝,則分配新的內(nèi)存遗遵;向上移動,則釋放那些內(nèi)存逸嘀。這種存儲方式速度僅次于寄存器车要。
(常用于存放對象引用和基本數(shù)據(jù)類型,而不用于存儲對象)
3.堆內(nèi)存
一種通用的內(nèi)存池崭倘,也位于RAM當中翼岁。其中存放的數(shù)據(jù)由JVM自動進行管理。
堆相對于棧的好處來說:編譯器不需要知道存儲的數(shù)據(jù)在堆里存活多長司光。當需要一個對象時琅坡,使用new寫一行代碼,當執(zhí)行這行代碼時残家,會自動在堆里進行存儲分配榆俺。同時,因為以上原因坞淮,用堆進行數(shù)據(jù)的存儲分配和清理茴晋,需要花費更多的時間。
4.常量池
常量(字符串常量和基本類型常量)通常直接存儲在程序代碼內(nèi)部(常量池)回窘。這樣做是安全的诺擅,因為它們的值在初始化時就已經(jīng)被確定,并不會被改變啡直。常量池在java用于保存在編譯期已確定的烁涌,已編譯的class文件中的一份數(shù)據(jù)。它包括了關(guān)于類付枫,方法烹玉,接口等中的常量,也包括字符串常量阐滩,如String
s = "java"這種申明方式
5.非RAM存儲區(qū)
如果數(shù)據(jù)完全存活于程序之外二打,那么它可以不受程序的任何控制,在程序沒有運行時也可以存在掂榔。其中兩個基本的例子是:流對象和持久化對象继效。
Java中數(shù)據(jù)的存儲分為以上5種方式,但在實際中最常談起的是:堆內(nèi)存存儲 與 棧內(nèi)存存儲装获。
我們可以聯(lián)系著二者來分析這兩種不同的存儲方式瑞信,更利于我們理解:
首先,它們有一定的相同之處:
堆與棧都是用于程序中的數(shù)據(jù)在RAM(內(nèi)存)上的存儲區(qū)域穴豫。并且Java會自動地管理堆和棧凡简,不能人為去直接設(shè)置逼友。
其次,更關(guān)鍵的在于它們的不同之處:
1.存儲數(shù)據(jù)類型:棧內(nèi)存中存放局部變量(基本數(shù)據(jù)類型和對象引用)秤涩,而堆內(nèi)存用于存放對象(實體)帜乞。
2.存儲速度:就存儲速度而言,棧內(nèi)存的存儲分配與清理速度更快于堆筐眷,并且棧內(nèi)存的存儲速度僅次于直接位于處理器當中的寄存器黎烈。
3.靈活性:就靈活性而言,由于棧內(nèi)存與堆內(nèi)存存儲機制的不同匀谣,堆內(nèi)存靈活性更優(yōu)于棧內(nèi)存照棋。
這樣兩種存儲方式的不同之處,也是由于它們自身的存儲機制所造成的武翎。所以為了理解它們烈炭,首先我們應該弄清楚它們分別的存儲原理和機制,在Java中:
— 棧內(nèi)存被要求存放在其中的數(shù)據(jù)的大小后频、生命周期必須是已經(jīng)確定的梳庆;
— 堆內(nèi)存可以被虛擬機動態(tài)的分配內(nèi)存大小暖途,無需事先告訴編譯器的數(shù)據(jù)的大小卑惜、生命周期等相關(guān)信息。
接下來便可以進行分析:
棧內(nèi)存和堆內(nèi)存的存儲數(shù)據(jù)類型為何不同驻售?
我們知道在Java中露久,變量的類型通常分為:基本數(shù)據(jù)類型變量和對象引用變量。
首先欺栗,8種基本數(shù)據(jù)類型中的數(shù)字類型實際上都是存儲的一組位數(shù)(所占bit位)不同的二進制數(shù)據(jù)毫痕;除此之外,布爾型只有true和false兩種可能值迟几。
其次消请,對象引用變量存儲的,實際是其所關(guān)聯(lián)(指向)對象在內(nèi)存中的內(nèi)存地址类腮,而內(nèi)存地址實際上也是一串二進制的數(shù)據(jù)臊泰。
所以,局部變量的大小是可以被確定的蚜枢;
接下來缸逃,java中,局部變量會在其自身所屬方法(或代碼塊)執(zhí)行完畢后厂抽,被自動釋放需频。
所以局部變量的生命周期也是可以被確定的。
那么筷凤,既然局部變量的大小和生命周期都可以被確定昭殉,完全符合棧內(nèi)存的存儲特點。自然,局部變量被存放在棧內(nèi)存中挪丢。
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
而Java中使用關(guān)鍵字new通過調(diào)用類的構(gòu)造函數(shù)莽鸭,從而得到該類的對象。
對象類型數(shù)據(jù)在程序編譯期吃靠,并不會在內(nèi)存中進行創(chuàng)建和存儲工作硫眨;而是在程序運行期,才根據(jù)需要進行動態(tài)的創(chuàng)建和存儲巢块。
也就是說礁阁,在程序運行之前,我們永遠不能確定這個對象的內(nèi)容族奢、大小姥闭、生命周期。自然越走,對象由堆內(nèi)存進行存儲管理棚品。
為什么棧內(nèi)存的速度高于堆內(nèi)存?
我個人是這樣理解的:
1.棧中數(shù)據(jù)大小和生命周期確定廊敌;堆中不確定铜跑。
2.說到大小,棧中存放的局部變量(8種基本數(shù)據(jù)類型和對象引用)實際值基本都是一串二進制數(shù)據(jù)骡澈,所以數(shù)據(jù)很小锅纺。而堆中存放的對象類型數(shù)據(jù)更大。
3.說到生命周期肋殴,棧中的數(shù)據(jù)在其所屬方法或代碼塊執(zhí)行結(jié)束后囤锉,就被釋放;而堆中的數(shù)據(jù)由垃圾回收機制進行管理护锤,無法確定合適會被回收釋放官地。
那么,一進行比較烙懦,很明顯的可以預見到:自身信息(大小和生命周期)確定驱入,數(shù)據(jù)大小更小的數(shù)據(jù)被處理起來肯定更加快捷,所以棧的存儲管理速度優(yōu)于堆修陡。
這就好比沧侥,明天要進行兩場考試:
第一場考試的試卷共有20道題,并且老師提前告訴了你所有題目魄鸦,你進行了復習宴杀。(你在考試之前(程序編譯期)已經(jīng)知道了試卷的信息)
第二場考試的試卷可能有50道甚至更多的題,并且老師沒有告訴你們?nèi)魏晤}目的信息拾因。(你只有在考試真正開始(程序運行期)才能知道試卷的信息)
得出的結(jié)論是什么旺罢?顯然相對于第一場考試旷余,完成第二場考試我們需要花費更多的時間。
為什么堆內(nèi)存的靈活性高于棧內(nèi)存扁达?
這就更好理解了正卧,一個要求數(shù)據(jù)的自身信息都必須被確定。一個可以動態(tài)的分配內(nèi)存大小跪解,也不必事先了解存儲數(shù)據(jù)的任何信息炉旷。
何為靈活性?也就是我們可以有更多的變數(shù)叉讥。那么對應的窘行,規(guī)則越多,限制則越強图仓,靈活性也就越弱罐盔。所以堆內(nèi)存的靈活性自然高于棧內(nèi)存。
除了上面的特點以外救崔,棧還有很重要的一個特點:棧內(nèi)存中存儲的數(shù)據(jù)可以實現(xiàn)數(shù)據(jù)共享惶看!
假設(shè)我們同時定義了兩個變量:? int a = 100; int b = 100;
這時候編譯器的工作過程是:首先會在棧中開辟一塊名為”a“的存儲空間,然后查看棧中是否存放著一個”100“的值六孵,發(fā)現(xiàn)在棧中沒有找到這樣的一個值纬黎,那么向棧中加入一個”100“的值,讓”a“等于這個值狸臣。繼而再在棧中開辟一塊名為”b“的存儲空間莹桅,這時候棧中已經(jīng)存在一個”100“的值昌执,那么就直接讓”b“也等于這個值就行了烛亦。
由此我們發(fā)現(xiàn),在完成對“a”的存儲分配后懂拾,再存儲“b”時煤禽,我們并沒有再次向柜子放進一個“100”,而是直接將前一次放進棧中的“100”的地址拿給“b”岖赋,棧里面”100“這個值同時功共享給了變量”a“和”b“檬果,這就是棧內(nèi)存中的數(shù)據(jù)共享。那么唐断,你可能會想选脊,實現(xiàn)數(shù)據(jù)共享的好處是什么?自然是節(jié)約內(nèi)存空間脸甘,既然同樣的值可以實現(xiàn)共享恳啥,那么就避免了反復像內(nèi)存中加入同樣的值。
那么丹诀,接下再看另一個例子(String類型的存儲是相對比較特殊的):
String s1 = "abc";
String s2 = "abc";
System.out.print(s1==s2);
這里的打印結(jié)果會是什么钝的?我們可能會這樣思考:
因為String是對象類型翁垂,定義了s1和s2兩個對象引用,分別指向值同樣為”abc“的兩個String類型對象硝桩。
Java中沿猜,”=="用于比較兩個對象引用時碗脊,實際是在比較這兩個引用是否指向同一個對象啼肩。
所以這里應該會打印false。但事實上衙伶,打印的結(jié)果為true疟游。這是由于什么原因造成的?
要搞清楚這個過程痕支,首先要理解:String s = "abc"和String s = new String("abc")兩張聲明方式的不同之處:
如果是使用String s = "abc"這種形式颁虐,也就是直接用雙引號定義的形式。
可以看做我們聲明了一個值為”abc“的字符串對象引用變量s卧须。
但是另绩,由于String類是final的,所以事實上花嘶,可以看做是聲明了一個字符串引用常量笋籽。存放在常量池中。
如果是使用關(guān)鍵字new這種形式聲明出的椭员,則是在程序運行期被動態(tài)創(chuàng)建车海,存放在堆中。
所以隘击,對于字符串而言侍芝,如果是編譯期已經(jīng)創(chuàng)建好(直接用雙引號定義的)的就存儲在常量池中;
如果是運行期(new出來的)才能確定的就存儲在堆中埋同。
對于equals相等的字符串州叠,在常量池中永遠只有一份,在堆中可以有多份凶赁。
了解了字符串存儲的這種特點咧栗,就可以對上面兩種不同的聲明方式進一步細化理解:
String s = ”abc“的工作過程可以分為以下幾個步驟:
(1)定義了一個名為"s"的String類型的引用。
(2)檢查在常量池中是否存在值為"abc"的字符串對象致板;
(3)如果不存在翰灾,則在常量池(字符串池)創(chuàng)建存儲進一個值為"abc"的字符串對象平斩。如果已經(jīng)存在,則跳過這一步工作晚凿。
(4)將對象引用s指向字符串池當中的”abc“對象。
String s = new String(”abc“)的步驟則為:
(1)定義了一個名為"s"的String類型的引用。
(2)檢查在常量池中是否存在值為"abc"的字符串對象;
(3)如果不存在,則在常量池(字符串池)存儲進一個值為"abc"的字符串對象。如果已經(jīng)存在,則跳過這一步工作蚁孔。
(4)在堆中創(chuàng)建存儲一個”abc“字符串對象。
(5)將對象引用指向堆中的對象摆尝。
這里指的注意的是,采用new的方式卫旱,雖然是在堆中存儲對象投放,但是也會在存儲之前檢查常量池中是否已經(jīng)含有此對象,如果沒有适贸,則會先在常量池創(chuàng)建對象跪呈,然后在堆中創(chuàng)建這個對象的”拷貝對象“。這也就是為什么有道面試題:String s = new String(“xyz”);產(chǎn)生幾個對象取逾?的答案是:一個或兩個的原因耗绿。因為如果常量池中原來沒有”xyz”,就是兩個。
弄清楚了原理砾隅,再看上面的例子误阻,就知道為什么了。在執(zhí)行String s1 = 'abc"時;常量池中還沒有對象晴埂,所以創(chuàng)建一個對象究反。之后在執(zhí)行String s2 = 'abc"的時候,因為常量池中已經(jīng)存在了"abc'對象儒洛,所以說s2只需要指向這個對象就完成工作了精耐。那么s1和s2指向同一個對象,用”==“比較自然返回true。所以常量池與棧內(nèi)存一樣琅锻,也可以實現(xiàn)數(shù)據(jù)共享卦停。
還有值得注意的一點的就是:我們知道局部變量存儲于棧內(nèi)存當中。那么成員變量呢恼蓬?答案是:成員變量的數(shù)據(jù)存儲于堆中該成員變量所屬的對象里面惊完。
而棧內(nèi)存與堆內(nèi)存的另一不同點在于,堆內(nèi)存中存放的變量都會進行默認初始化处硬,而棧內(nèi)存中存放的變量卻不會小槐。
這也就是為什么,我們在聲明一個成員變量時荷辕,可以不用對其進行初始化賦值凿跳。而如果聲明一個局部變量卻未進行初始賦值件豌,如果想對其進行使用就會報編譯異常的原因了。
最后控嗜,借助網(wǎng)上看到的一個例子幫助對棧內(nèi)存茧彤,堆內(nèi)存的存儲進行理解:
class BirthDate {
private int day;
private int month;
private int year;
public BirthDate(int d, int m, int y) {
day = d;
month = m;
year = y;
}
省略get,set方法………
}
public class Test{
public static void main(String args[]){
int date = 9;
Test test = new Test();
test.change(date);
BirthDate d1= new BirthDate(7,7,1970);
}
public void change1(int i){
i = 1234;
}
}
對于以上這段代碼,date為局部變量躬审,i,d,m,y都是形參為局部變量棘街,day,month承边,year為成員變量遭殉。下面分析一下代碼執(zhí)行時候的變化:
1. main方法開始執(zhí)行:int date = 9;
date局部變量,基礎(chǔ)類型博助,引用和值都存在棧中险污。
2. Test test = new Test();
test為對象引用,存在棧中富岳,對象(new Test())存在堆中蛔糯。
3. test.change(date);
調(diào)用change(int i)方法,i為局部變量窖式,引用和值存在棧中蚁飒。當方法change執(zhí)行完成后,i就會從棧中消失萝喘。
4. BirthDate d1= new BirthDate(7,7,1970);
調(diào)用BIrthDate類的構(gòu)造函數(shù)生成對象淮逻。
d1為對象引用,存在棧中阁簸;
對象(new BirthDate())存在堆中爬早;
其中d,m,y為局部變量存儲在棧中,且它們的類型為基礎(chǔ)類型启妹,因此它們的數(shù)據(jù)也存儲在棧中筛严;
day,month,year為BirthDate對象的的成員變量,它們存儲在堆中存儲的new BirthDate()對象里面饶米;
當BirthDate構(gòu)造方法執(zhí)行完之后桨啃,d,m,y將從棧中消失。
5.main方法執(zhí)行完之后咙崎。
date變量优幸,test,d1引用將從棧中消失褪猛;
new Test(),new BirthDate()將等待垃圾回收器進行回收。
轉(zhuǎn)自:http://www.xuebuyuan.com/2225286.html