java的存儲方式

原文鏈接:https://blog.csdn.net/qq_36838191/article/details/80248182

(1) 寄存器。這是最快的保存區域,因爲它位於和其他所有保存方式不同的地方:處理器內部。然而,寄存器的數量十分有限,所以寄存器是根據需要由編譯器分配。我們對此沒有直接的控制權,也不可能在自己的程序裏找到寄存器存在的任何蹤跡。

(2) 堆棧。駐留於常規 RAM(隨機訪問存儲器)區域,但可通過它的“堆棧指針”獲得處理的直接支持。堆棧指針若向下移,會創建新的內存;若向上移,則會釋放那些內存。 這是一種特別快、 特別有效的數據保存方式, 僅次於寄存器。創建程序時,Java 編譯器必須準確地知道堆棧內保存的所有數據的“長度”以及“存在時間” 。這是由於它必須生成相應的代碼,以便向上和向下移動指針。這一限制無疑影響了程序的靈活性,所以儘管有些 Java 數據要保存在堆棧裏——特別是對象句柄,但 Java 對象並不放到其中。

(3) 堆。一種常規用途的內存池(也在 RAM 區域) ,其中保存了 Java 對象。和堆棧不同, “內存堆”或“堆” (Heap)最吸引人的地方在於編譯器不必知道要從堆裏分配多少存儲空間,也不必知道存儲的數據要在堆裏停留多長的時間。因此,用堆保存數據時會得到更大的靈活性。要求創建一個對象時,只需用 new命令編制相關的代碼即可。執行這些代碼時,會在堆裏自動進行數據的保存。當然,爲達到這種靈活性,必然會付出一定的代價:在堆裏分配存儲空間時會花掉更長的時間!

(4) 靜態存儲。這兒的“靜態” (Static)是指“位於固定位置” (儘管也在RAM 裏) 。程序運行期間,靜態存儲的數據將隨時等候調用。可用 static 關鍵字指出一個對象的特定元素是靜態的。但 Java 對象本身永遠都不會置入靜態存儲空間。

(5) 常數存儲。常數值通常直接置於程序代碼內部。這樣做是安全的,因爲它們永遠都不會改變。有的常數需要嚴格地保護,所以可考慮將它們置入只讀存儲器(ROM) 。

(6) 非 RAM 存儲。若數據完全獨立於一個程序之外,則程序不運行時仍可存在, 並在程序的控制範圍之外。 其中兩個最主要的例子便是 “流式對象” 和 “固定對象” 。對於流式對象,對象會變成字節流,通常會發給另一臺機器。而對於固定對象,對象保存在磁盤中。即使程序中止運行,它們仍可保持自己的狀態不變。對於這些類型的數據存儲,一個特別有用的技巧就是它們能存在於其他媒體中。一旦需要,甚至能將它們恢復成普通的、基於 RAM 的對象。Java 1.1 提供了對 Lightweight persistence 的支持。未來的版本甚至可能提供更完整的方案。 java的內存機制

Java 把內存劃分成兩種:一種是棧內存,另一種是堆內存。

在函數中定義的一些基本類型的變量和對象的引用變量都是在函數的棧內存中分配,當在一段代碼塊定義一個變量時,Java 就在棧中爲這個變量分配內存空間,當超過變量的作用域後,Java 會自動釋放掉爲該變量分配的內存空間,該內存空間可以立即被另作它用。
  堆內存用來存放由 new 創建的對象和數組,在堆中分配的內存,由 Java 虛擬機的自動垃圾回收器來管理。在堆中產生了一個數組或者對象之後,還可以在棧中定義一個特殊的變量,讓棧中的這個變量的取值等於數組或對象在堆內存中的首地址,棧中的這個變量就成了數組或對象的引用變量,以後就可以在程序中使用棧中的引用變量來訪問堆中的數組或者對象,引用變量就相當於是爲數組或者對象起的一個名稱。引用變量是普通的變量,定義時在棧中分配,引用變量在程序運行到其作用域之外後被釋放。而數組和對象本身在堆中分配,即使程序運行到使用 new 產生數組或者對象的語句所在的代碼塊之外,數組和對象本身佔據的內存不會被釋放,數組和對象在沒有引用變量指向它的時候,才變爲垃圾,不能在被使用,但仍然佔據內存空間不放,在隨後的一個不確定的時間被垃圾回收器收走(釋放掉)。

這也是 Java 比較佔內存的原因,實際上,棧中的變量指向堆內存中的變量,這就是 Java 中的指針! 堆棧、堆緩存方式區別

1、棧使用的是一級緩存, 他們通常都是被調用時處於存儲空間中,調用完畢立即釋放; 2、堆棧是存放在二級緩存中,生命週期由虛擬機的垃圾回收算法來決定(並不是一旦成爲孤兒對象就能被回收)。所以調用這些對象的速度要相對來得低一些。

堆和棧的理論知識

1、 申請方式

堆棧: 由系統自動分配

堆:需要程序員自己申請

2.2、申請後系統的響應

堆棧:只要棧的剩餘空間大於所申請空間,系統將爲程序提供內存,否則將報異常提示棧溢出。

堆:首先應該知道操作系統有一個記錄空閒內存地址的鏈表,當系統收到程序的申請時,會遍歷該鏈表,尋找第一個空間大於所申請空間的堆結點,然後將該結點從空閒結點鏈表中刪除,並將該結點的空間分配給程序,另外,對於大多數系統,會在這塊內存空間中的首地址處記錄本次分配的大小,這樣,代碼中的delete語句才能正確的釋放本內存空間。另外,由於找到的堆結點的大小不一定正好等於申請的大小,系統會自動的將多餘的那部 分重新放入空閒鏈表中。

2.3、申請大小的限制

堆棧:在Windows下,棧是向低地址擴展的數據結構,是一塊連續的內存的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在WINDOWS下,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩餘空間時,將提示overflow。因此,能從棧獲得的空間較小。

堆:堆是向高地址擴展的數據結構,是不連續的內存區域。這是由於系統是用鏈表來存儲 的空閒內存地址的,自然是不連續的,而鏈表的遍歷方向是由低地址向高地址。堆的大小 受限於計算機系統中有效的虛擬內存。由此可見,堆獲得的空間比較靈活,也比較大。

2.4申請效率的比較:

堆棧由系統自動分配,速度較快。但程序員是無法控制的。

堆是由new分配的內存,一般速度比較慢,而且容易產生內存碎片,不過用起來最方便。

Java中的數據類型有兩種。 一種是基本類型(primitive types), 共有8種,即int, short, long, byte, float, double, boolean, char(注意,並沒有string的基本類型)。這種類型的定義是通過諸如int a = 3; long b = 255L;的形式來定義的,稱爲自動變量。值得注意的是,自動變量存的是字面值,不是類的實例,即不是類的引用,這裏並沒有類的存在。如int a = 3; 這裏的a是一個指向int類型的引用,指向3這個字面值。這些字面值的數據,由於大小可知,生存期可知(這些字面值存儲在常數存儲區)

另一種是包裝類數據,如Integer, String, Double等將相應的基本數據類型包裝起來的類。這些類數據全部存在於堆中,Java用new()語句來顯式地告訴編譯器,在運行時才根據需要動態創建,因此比較靈活,但缺點是要佔用更多的時間。

特殊情況:基本類型

有一系列類需特別對待;可將它們想象成“基本” 、 “主要”或者“主”(Primitive)類型,進行程序設計時要頻繁用到它們。之所以要特別對待,是由於用 new 創建對象(特別是小的、簡單的變量)並不是非常有效,因爲 new 將對象置於“堆”裏。對於這些類型,Java 採納了與 C 和 C++相同的方法。也就是說,不是用 new 創建變量,而是創建一個並非句柄的“自動”變量。這個變量容納了具體的值,並置於堆棧中,能夠更高效地存取。Java 決定了每種主要類型的大小。就象在大多數語言裏那樣,這些大小並不隨着機器結構的變化而變化。這種大小的不可更改正是 Java 程序具有很強移植能力的原因之一。

主類型(放置於堆棧內) 大小 最小值 最大值 封裝器類型(放置於堆內)

boolean 1 位 - - Boolean

char 16 位 Unicode 0 Unicode 2 的 16 次方-1 Character

byte 8 位 -128 +127 Byte(註釋①)

short 16 位 -2 的 15 次方 +2 的 15 次方-1 Short(註釋①)

int 32 位 -2 的 31 次方 +2 的 31 次方-1 Integer

long 64 位 -2 的 63 次方 +2 的 63 次方-1 Long

float 32 位 IEEE754 IEEE754 Float

double 64 位 IEEE754 IEEE754 Double

Void - - - Void(註釋①)

①:到 Java 1.1 纔有,1.0 版沒有。

數值類型全都是有符號(正負號)的,所以不必費勁尋找沒有符號的類型。主數據類型也擁有自己的“封裝器” (wrapper)類。這意味着假如想讓堆內一個非主要對象表示那個主類型,就要使用對應的封裝器。

Java中數據的存儲分爲以上6種方式,但在實際中最常談起的是:堆內存存儲 與 棧內存存儲。

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

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

堆與棧都是用於程序中的數據在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爲成員變量。下面分析一下代碼執行時候的變化:

main方法開始執行:int date = 9; date局部變量,基礎類型,引用和值都存在棧中。
Test test = new Test(); test爲對象引用,存在棧中,對象(new Test())存在堆中。
test.change(date); 調用change(int i)方法,i爲局部變量,引用和值存在棧中。當方法change執行完成後,i就會從棧中消失。
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()將等待垃圾回收器進行回收。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章