Java五種儲存位置
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會自動地管理堆和棧,不能人為去直接設定。
區別:
- 儲存資料型別:棧記憶體中存放區域性變數(基本資料型別和物件引用),而堆記憶體用於存放物件(實體)。
- 儲存速度:就儲存速度而言,棧記憶體的儲存分配與清理速度更快於堆,並且棧記憶體的儲存速度僅次於直接位於處理器當中的暫存器。
- 靈活性:就靈活性而言,由於棧記憶體與堆記憶體儲存機制的不同,堆記憶體靈活性更優於棧記憶體。
2.2 儲存機制
- 棧記憶體被要求存放在其中的資料的大小、生命週期必須是已經確定的;
- 堆記憶體可以被虛擬機器動態的分配記憶體大小,無需事先告訴編譯器的資料的大小、生命週期等相關資訊。
- 棧記憶體和堆記憶體的儲存資料型別為何不同?
我們知道在Java中,變數的型別通常分為:基本資料型別變數和物件引用變數。
首先,8種基本資料型別中的數字型別實際上都是儲存的一組位數(所佔bit位)不同的二進位制資料;除此之外,布林型只有true和false兩種可能值。
其次,物件引用變數儲存的,實際是其所關聯(指向)物件在記憶體中的記憶體地址,而記憶體地址實際上也是一串二進位制的資料。
所以,區域性變數的大小是可以被確定的;
接下來,java中,區域性變數會在其自身所屬方法(或程式碼塊)執行完畢後,被自動釋放。
所以區域性變數的生命週期也是可以被確定的。
那麼,既然區域性變數的大小和生命週期都可以被確定,完全符合棧記憶體的儲存特點。自然,區域性變數被存放在棧記憶體中。
而Java中使用關鍵字new通過呼叫類的建構函式,從而得到該類的物件。
物件型別資料在程式編譯期,並不會在記憶體中進行建立和儲存工作;而是在程式執行期,才根據需要進行動態的建立和儲存。
也就是說,在程式執行之前,我們永遠不能確定這個物件的內容、大小、生命週期。自然,物件由堆記憶體進行儲存管理。
- 為什麼棧記憶體的速度高於堆記憶體?
1.棧中資料大小和生命週期確定;堆中不確定。
2.說到大小,棧中存放的區域性變數(8種基本資料型別和物件引用)實際值基本都是一串二進位制資料,所以資料很小。而堆中存放的物件型別資料更大。
3.說到生命週期,棧中的資料在其所屬方法或程式碼塊執行結束後,就被釋放;而堆中的資料由垃圾回收機制進行管理,無法確定合適會被回收釋放。
那麼,一進行比較,很明顯的可以預見到:自身資訊(大小和生命週期)確定,資料大小更小的資料被處理起來肯定更加快捷,所以棧的儲存管理速度優於堆。
- 為什麼堆記憶體的靈活性高於棧記憶體?
這就更好理解了,一個要求資料的自身資訊都必須被確定。一個可以動態的分配記憶體大小,也不必事先了解儲存資料的任何資訊。
何為靈活性?也就是我們可以有更多的變數。那麼對應的,規則越多,限制則越強,靈活性也就越弱。所以堆記憶體的靈活性自然高於棧記憶體。
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;
}
對於以上這段程式碼,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將從棧中消失。 - main方法執行完之後。
date變數,test,d1引用將從棧中消失;
new Test(),new BirthDate()將等待垃圾回收器進行回收。
相關文章
- 修改docker的預設儲存位置及映象儲存位置Docker
- 【Java小疑問】java變數儲存的位置(雜)Java變數
- Podman修改 image儲存位置
- java物件頭的兩種儲存Java物件
- win10桌面儲存位置怎麼修改_win10修改桌面儲存位置教程Win10
- 修改docker映象儲存位置的方法Docker
- gitlab資料庫儲存位置Gitlab資料庫
- SettingsProvider資料儲存位置IDE
- RAC變更ASMSPFILE儲存位置(轉)ASM
- SAPI訓練檔案儲存位置API
- store下載檔案儲存位置
- wsl遷移儲存位置(轉載)
- Python常用的資料儲存方式有哪些?五種!Python
- 【Java】儲存Java
- Git儲存內容的位置與方式Git
- Docker: 如何修改 Docker 的映象儲存位置Docker
- Python 中整型物件儲存的位置Python物件
- 系統統計資訊的儲存位置
- HTML5的五種客戶端離線儲存方案HTML客戶端
- 本地儲存VS雲端儲存:區別不只是資料存放位置
- Mac儲存檔案找不到要儲存的位置怎麼設定Mac
- Mac電腦如何更改截圖儲存位置Mac
- Docker配置本地映象與容器的儲存位置Docker
- 修改MySQL資料庫儲存位置datadirMySql資料庫
- Redis資料儲存位置匯出資料Redis
- 理解Python中整型物件儲存的位置Python物件
- 三種儲存結構
- HTML5 Web 客戶端五種離線儲存方式彙總HTMLWeb客戶端
- SQL總結(五)儲存過程SQL儲存過程
- java儲存過程Java儲存過程
- 如何更新Mac電腦截圖的儲存位置?Mac
- Laravel 5.8 自定義位置日誌按天儲存Laravel
- 如何獲取HDFS上檔案的儲存位置
- 微信資料夾儲存在什麼位置?如何修改儲存路徑
- wordpress外掛在伺服器上的儲存位置伺服器
- 何謂“第五代儲存”?
- OS學習筆記五:儲存模型筆記模型
- Android的3種資料儲存技術(一)File儲存Android