1. 程式人生 > >java中資料的5種儲存位置(堆與棧)

java中資料的5種儲存位置(堆與棧)

任何語言所編寫的程式,其中的各型別的資料都需要一個儲存位置,Java中資料的儲存位置分為以下5種:

1.暫存器

最快的儲存區,位於處理器內部,但是數量極其有限。所以暫存器根據需求進行自動分配,無法直接人為控制。

2.棧記憶體

位於RAM當中,通過堆疊指標可以從處理器獲得直接支援。堆疊指標向下移動,則分配新的記憶體;向上移動,則釋放那些記憶體。這種儲存方式速度僅次於暫存器。

(常用於存放物件引用基本資料型別,而不用於儲存物件

3.堆記憶體

一種通用的記憶體池,也位於RAM當中。其中存放的資料由JVM自動進行管理。

堆相對於棧的好處來說:編譯器不需要知道儲存的資料在堆裡存活多長

。當需要一個物件時,使用new寫一行程式碼,當執行這行程式碼時,會自動在堆裡進行儲存分配。同時,因為以上原因,用堆進行資料的儲存分配和清理,需要花費更多的時間

4.常量池

常量(字串常量和基本型別常量)通常直接儲存在程式程式碼內部(常量池)。這樣做是安全的,因為它們的值在初始化時就已經被確定,並不會被改變。常量池在java用於儲存在編譯期已確定的,已編譯的class檔案中的一份資料。它包括了關於類,方法,介面等中的常量,也包括字串常量,如String s = "java"這種申明方式

5.非RAM儲存區

如果資料完全存活於程式之外,那麼它可以不受程式的任何控制,在程式沒有執行時也可以存在。其中兩個基本的例子是:流物件和持久化物件。


Java中資料的儲存分為以上5種方式,但在實際中最常談起的是:堆記憶體儲存 與 棧記憶體儲存。

我們可以聯絡著二者來分析這兩種不同的儲存方式,更利於我們理解:

首先,它們有一定的相同之處

堆與棧都是用於程式中的資料在RAM(記憶體)上的儲存區域。並且Java會自動地管理堆和棧,不能人為去直接設定。

其次,更關鍵的在於它們的不同之處

1.儲存資料型別:棧記憶體中存放區域性變數基本資料型別物件引用),而堆記憶體用於存放物件(實體)。

2.儲存速度:就儲存速度而言,棧記憶體的儲存分配與清理速度更快於堆,並且棧記憶體的儲存速度僅次於直接位於處理器當中的暫存器。

3.靈活性:

就靈活性而言,由於棧記憶體與堆記憶體儲存機制的不同,堆記憶體靈活性更優於棧記憶體。

這樣兩種儲存方式的不同之處,也是由於它們自身的儲存機制所造成的。所以為了理解它們,首先我們應該弄清楚它們分別的儲存原理和機制,在Java中:

— 棧記憶體被要求存放在其中的資料的大小、生命週期必須是已經確定的;

— 堆記憶體可以被虛擬機器動態的分配記憶體大小,無需事先告訴編譯器的資料的大小、生命週期等相關資訊。

接下來便可以進行分析:

棧記憶體和堆記憶體的儲存資料型別為何不同?

我們知道在Java中,變數的型別通常分為:基本資料型別變數和物件引用變數。

首先,8種基本資料型別中的數字型別實際上都是儲存的一組位數(所佔bit位)不同的二進位制資料;除此之外,布林型只有true和false兩種可能值。

其次,物件引用變數儲存的,實際是其所關聯(指向)物件在記憶體中的記憶體地址,而記憶體地址實際上也是一串二進位制的資料。

所以,區域性變數的大小是可以被確定的;

接下來,java中,區域性變數會在其自身所屬方法(或程式碼塊)執行完畢後,被自動釋放。

所以區域性變數的生命週期也是可以被確定的。

那麼,既然區域性變數的大小和生命週期都可以被確定,完全符合棧記憶體的儲存特點。自然,區域性變數被存放在棧記憶體中。

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

而Java中使用關鍵字new通過呼叫類的建構函式,從而得到該類的物件。

物件型別資料在程式編譯期,並不會在記憶體中進行建立和儲存工作;而是在程式執行期,才根據需要進行動態的建立和儲存。

也就是說,在程式執行之前,我們永遠不能確定這個物件的內容、大小、生命週期。自然,物件由堆記憶體進行儲存管理。

為什麼棧記憶體的速度高於堆記憶體?

我個人是這樣理解的:

1.棧中資料大小和生命週期確定;堆中不確定。

2.說到大小,棧中存放的區域性變數(8種基本資料型別和物件引用)實際值基本都是一串二進位制資料,所以資料很小。而堆中存放的物件型別資料更大。

3.說到生命週期,棧中的資料在其所屬方法或程式碼塊執行結束後,就被釋放;而堆中的資料由垃圾回收機制進行管理,無法確定合適會被回收釋放。

那麼,一進行比較,很明顯的可以預見到:自身資訊(大小和生命週期)確定,資料大小更小的資料被處理起來肯定更加快捷,所以棧的儲存管理速度優於堆。

這就好比,明天要進行兩場考試:

第一場考試的試卷共有20道題,並且老師提前告訴了你所有題目,你進行了複習。(你在考試之前(程式編譯期)已經知道了試卷的資訊)

第二場考試的試卷可能有50道甚至更多的題,並且老師沒有告訴你們任何題目的資訊。(你只有在考試真正開始(程式執行期)才能知道試卷的資訊)

得出的結論是什麼?顯然相對於第一場考試,完成第二場考試我們需要花費更多的時間。

為什麼堆記憶體的靈活性高於棧記憶體?

這就更好理解了,一個要求資料的自身資訊都必須被確定。一個可以動態的分配記憶體大小,也不必事先了解儲存資料的任何資訊。

何為靈活性?也就是我們可以有更多的變數。那麼對應的,規則越多,限制則越強,靈活性也就越弱。所以堆記憶體的靈活性自然高於棧記憶體。


除了上面的特點以外,棧還有很重要的一個特點:棧記憶體中儲存的資料可以實現資料共享!

假設我們同時定義了兩個變數:  int a = 100; int b = 100;

這時候編譯器的工作過程是:首先會在棧中開闢一塊名為”a“的儲存空間,然後檢視棧中是否存放著一個”100“的值,發現在棧中沒有找到這樣的一個值,那麼向棧中加入一個”100“的值,讓”a“等於這個值。繼而再在棧中開闢一塊名為”b“的儲存空間,這時候棧中已經存在一個”100“的值,那麼就直接讓”b“也等於這個值就行了。

由此我們發現,在完成對“a”的儲存分配後,再儲存“b”時,我們並沒有再次向櫃子放進一個“100”,而是直接將前一次放進棧中的“100”的地址拿給“b”,棧裡面”100“這個值同時功共享給了變數”a“和”b“,這就是棧記憶體中的資料共享。那麼,你可能會想,實現資料共享的好處是什麼?自然是節約記憶體空間,既然同樣的值可以實現共享,那麼就避免了反覆像記憶體中加入同樣的值。


那麼,接下再看另一個例子(String型別的儲存是相對比較特殊的):

String s1 = "abc";

String s2 = "abc";

System.out.print(s1==s2);

這裡的列印結果會是什麼?我們可能會這樣思考:

因為String是物件型別,定義了s1和s2兩個物件引用,分別指向值同樣為”abc“的兩個String型別物件。

Java中,”=="用於比較兩個物件引用時,實際是在比較這兩個引用是否指向同一個物件。

所以這裡應該會列印false。但事實上,列印的結果為true。這是由於什麼原因造成的?


要搞清楚這個過程,首先要理解:String s = "abc"和String s = new String("abc")兩張宣告方式的不同之處:

如果是使用String s = "abc"這種形式,也就是直接用雙引號定義的形式。

可以看做我們聲明瞭一個值為”abc“的字串物件引用變數s。

但是,由於String類是final的,所以事實上,可以看做是聲明瞭一個字串引用常量。存放在常量池中。

如果是使用關鍵字new這種形式宣告出的,則是在程式執行期被動態建立,存放在堆中。


所以,對於字串而言,如果是編譯期已經建立好(直接用雙引號定義的)的就儲存在常量池中;

如果是執行期(new出來的)才能確定的就儲存在中。

對於equals相等的字串,在常量池中永遠只有一份在堆中可以有多份


瞭解了字串儲存的這種特點,就可以對上面兩種不同的宣告方式進一步細化理解:

String s = ”abc“的工作過程可以分為以下幾個步驟

  (1)定義了一個名為"s"的String型別的引用。

  (2)檢查在常量池中是否存在值為"abc"的字串物件;

  (3)如果不存在,則在常量池(字串池)建立儲存進一個值為"abc"的字串物件。如果已經存在,則跳過這一步工作。

  (4)將物件引用s指向字串池當中的”abc“物件。

String s = new String(”abc“)的步驟則為

  (1)定義了一個名為"s"的String型別的引用。

  (2)檢查在常量池中是否存在值為"abc"的字串物件;

  (3)如果不存在,則在常量池(字串池)儲存進一個值為"abc"的字串物件。如果已經存在,則跳過這一步工作。

  (4)在堆中建立儲存一個”abc“字串物件。

  (5)將物件引用指向堆中的物件。

這裡指的注意的是,採用new的方式,雖然是在堆中儲存物件,但是也會在儲存之前檢查常量池中是否已經含有此物件,如果沒有,則會先在常量池建立物件,然後在堆中建立這個物件的”拷貝物件“。這也就是為什麼有道面試題:String s = new String(“xyz”);產生幾個物件?的答案是:一個或兩個的原因。因為如果常量池中原來沒有”xyz”,就是兩個。

弄清楚了原理,再看上面的例子,就知道為什麼了。在執行String s1 = 'abc"時;常量池中還沒有物件,所以建立一個物件。之後在執行String s2 = 'abc"的時候,因為常量池中已經存在了"abc'物件,所以說s2只需要指向這個物件就完成工作了。那麼s1和s2指向同一個物件,用”==“比較自然返回true。所以常量池與棧記憶體一樣,也可以實現資料共享。


還有值得注意的一點的就是:我們知道區域性變數儲存於棧記憶體當中。那麼成員變數呢?答案是:成員變數的資料儲存於堆中該成員變數所屬的物件裡面

而棧記憶體與堆記憶體的另一不同點在於,堆記憶體中存放的變數都會進行預設初始化,而棧記憶體中存放的變數卻不會。

這也就是為什麼,我們在宣告一個成員變數時,可以不用對其進行初始化賦值。而如果宣告一個區域性變數卻未進行初始賦值,如果想對其進行使用就會報編譯異常的原因了。


最後,藉助網上看到的一個例子幫助對棧記憶體,堆記憶體的儲存進行理解:

    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為成員變數。下面分析一下程式碼執行時候的變化:
1. main方法開始執行:int date = 9;
date區域性變數,基礎型別,引用和值都存在棧中
2. Test test = new Test();
test為物件引用,存在棧中,物件(new Test())存在堆中。
3. test.change(date);
呼叫change(int i)方法,i為區域性變數,引用和值存在棧中。當方法change執行完成後,i就會從棧中消失。

4. BirthDate d1= new BirthDate(7,7,1970);  

呼叫BIrthDate類的建構函式生成物件。

d1為物件引用,存在棧中;

物件(new BirthDate())存在堆中;

其中d,m,y為區域性變數儲存在棧中,且它們的型別為基礎型別,因此它們的資料也儲存在棧中;

day,month,year為BirthDate物件的的成員變數,它們儲存在堆中儲存的new BirthDate()物件裡面;

當BirthDate構造方法執行完之後,d,m,y將從棧中消失。

5.main方法執行完之後。

date變數,test,d1引用將從棧中消失;

new Test(),new BirthDate()將等待垃圾回收器進行回收。