Java五種儲存位置

weixin_34007291發表於2017-07-27

1. 五種儲存位置

1.1 暫存器

最快的儲存區,位於處理器中,數量及其有限。所以暫存器根據需求進行分配,不能人為控制。

1.2 棧

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

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

1.3 堆

一種通用的記憶體池,也位於RAM當中。其中存放的資料由JVM自動進行管理。
堆相對於棧的好處來說:編譯器不需要知道儲存的資料在堆裡存活多長。當需要一個物件時,使用new寫一行程式碼,當執行這行程式碼時,會自動在堆裡進行儲存分配。同時,因為以上原因,用堆進行資料的儲存分配和清理,需要花費更多的時間。

(常用於儲存物件)

1.4 常量池

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

1.5 非RAM儲存

如果資料完全存活於程式之外,那麼它可以不受程式的任何控制,在程式沒有執行時也可以存在。其中兩個基本的例子是:流物件和持久化物件。在流物件中,物件轉化為位元組流,通常被髮送給另一臺機器。在持久化物件中,物件被存放在磁碟上。因此,即使程式終止,它們仍可以保持自己的狀態。

2. 堆和棧詳解

2.1 兩者相同點和不同點

相同之處:

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

區別:

  1. 儲存資料型別:棧記憶體中存放區域性變數(基本資料型別和物件引用),而堆記憶體用於存放物件(實體)。
  2. 儲存速度:就儲存速度而言,棧記憶體的儲存分配與清理速度更快於堆,並且棧記憶體的儲存速度僅次於直接位於處理器當中的暫存器。
  3. 靈活性:就靈活性而言,由於棧記憶體與堆記憶體儲存機制的不同,堆記憶體靈活性更優於棧記憶體。

2.2 儲存機制

  • 棧記憶體被要求存放在其中的資料的大小、生命週期必須是已經確定的;
  • 堆記憶體可以被虛擬機器動態的分配記憶體大小,無需事先告訴編譯器的資料的大小、生命週期等相關資訊。
  1. 棧記憶體和堆記憶體的儲存資料型別為何不同?

我們知道在Java中,變數的型別通常分為:基本資料型別變數和物件引用變數。
首先,8種基本資料型別中的數字型別實際上都是儲存的一組位數(所佔bit位)不同的二進位制資料;除此之外,布林型只有true和false兩種可能值。
其次,物件引用變數儲存的,實際是其所關聯(指向)物件在記憶體中的記憶體地址,而記憶體地址實際上也是一串二進位制的資料。
所以,區域性變數的大小是可以被確定的;
接下來,java中,區域性變數會在其自身所屬方法(或程式碼塊)執行完畢後,被自動釋放。
所以區域性變數的生命週期也是可以被確定的。
那麼,既然區域性變數的大小和生命週期都可以被確定,完全符合棧記憶體的儲存特點。自然,區域性變數被存放在棧記憶體中。

而Java中使用關鍵字new通過呼叫類的建構函式,從而得到該類的物件。
物件型別資料在程式編譯期,並不會在記憶體中進行建立和儲存工作;而是在程式執行期,才根據需要進行動態的建立和儲存。
也就是說,在程式執行之前,我們永遠不能確定這個物件的內容、大小、生命週期。自然,物件由堆記憶體進行儲存管理。

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

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

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

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

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

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

這就更好理解了,一個要求資料的自身資訊都必須被確定。一個可以動態的分配記憶體大小,也不必事先了解儲存資料的任何資訊。
何為靈活性?也就是我們可以有更多的變數。那麼對應的,規則越多,限制則越強,靈活性也就越弱。所以堆記憶體的靈活性自然高於棧記憶體。

3. 資料共享

棧和常量池都有一個特點就是共享資料。

假設我們同時定義了兩個變數: int a = 100; int b = 100;
這時候編譯器的工作過程是:首先會在棧中開闢一塊名為”a“的儲存空間,然後檢視棧中是否存放著一個”100“的值,發現在棧中沒有找到這樣的一個值,那麼向棧中加入一個”100“的值,讓”a“等於這個值。繼而再在棧中開闢一塊名為”b“的儲存空間,這時候棧中已經存在一個”100“的值,那麼就直接讓”b“也等於這個值就行了。
由此我們發現,在完成對“a”的儲存分配後,再儲存“b”時,我們並沒有再次向櫃子放進一個“100”,而是直接將前一次放進棧中的“100”的地址拿給“b”,棧裡面”100“這個值同時功共享給了變數”a“和”b“,這就是棧記憶體中的資料共享。那麼,你可能會想,實現資料共享的好處是什麼?自然是節約記憶體空間,既然同樣的值可以實現共享,那麼就避免了反覆向記憶體中加入同樣的值。
定義完a與b的值後,再令a = 4;那麼,b不會等於4,還是等於100。在編譯器內部,遇到時,它就會重新搜尋棧中是否有4的字面值,如果沒有,重新開闢地址存放4的值;如果已經有了,則直接將a指向這個地址。因此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。所以常量池與棧記憶體一樣,也可以實現資料共享。

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

而棧記憶體與堆記憶體的另一不同點在於,堆記憶體中存放的變數都會進行預設初始化,而棧記憶體中存放的變數卻不會。
這也就是為什麼,我們在宣告一個成員變數時,可以不用對其進行初始化賦值。而如果宣告一個區域性變數卻未進行初始賦值,如果想對其進行使用就會報編譯異常的原因了。

4. 例項

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;    
       }   
image

對於以上這段程式碼,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()將等待垃圾回收器進行回收。

相關文章