1. 程式人生 > 實用技巧 >關於jvm中的儲存機制(棧、堆、方法區和常量池)

關於jvm中的儲存機制(棧、堆、方法區和常量池)

先放圖

一、java的六種儲存地址及解釋

1) 暫存器(register):這是最快的儲存區,因為它位於不同於其他儲存區的地方——處理器內部。但是暫存器的數量極其有限,所以暫存器由編譯器根據需求進行分配。你不能直接控制,也不能在程式中感覺到暫存器存在的任何跡象。

2) 堆疊(stack):位於通用RAM中,但通過它的“堆疊指標”可以從處理器哪裡獲得支援。堆疊指標若向下移動,則分配新的記憶體;若向上移動,則釋放那些記憶體。這是一種快速有效的分配儲存方法,僅次於暫存器。建立程式時候,JAVA編譯器必須知道儲存在堆疊內所有資料的確切大小和生命週期,因為它必須生成相應的程式碼,以便上下移動堆疊指標。這一約束限制了程式的靈活性,所以雖然某些JAVA資料儲存在堆疊中——特別是物件引用,但是JAVA物件不儲存其中。

3)堆(heap):一種通用性的記憶體池(也存在於RAM中),用於存放所有的JAVA物件。堆不同於堆疊的好處是:編譯器不需要知道要從堆裡分配多少儲存區域,也不必知道儲存的資料在堆裡存活多長時間。因此,在堆裡分配儲存有很大的靈活性。當你需要建立一個物件的時候,只需要new寫一行簡單的程式碼,當執行這行程式碼時,會自動在堆裡進行儲存分配。當然,為這種靈活性必須要付出相應的程式碼。用堆進行儲存分配比用堆疊進行儲存儲存需要更多的時間。

4)靜態儲存(static storage):這裡的“靜態”是指“在固定的位置”。靜態儲存裡存放程式執行時一直存在的資料。你可用關鍵字static來標識一個物件的特定元素是靜態的,但JAVA物件本身從來不會存放在靜態儲存空間裡。

5) 常量儲存(constant storage):常量值通常直接存放在程式程式碼內部,這樣做是安全的,因為它們永遠不會被改變。有時,在嵌入式系統中,常量本身會和其他部分分割離開,所以在這種情況下,可以選擇將其放在ROM中。

6) 非RAM儲存:如果資料完全存活於程式之外,那麼它可以不受程式的任何控制,在程式沒有執行時也可以存在。

二、棧、堆、方法區儲存的內容

棧區:
1.每個執行緒包含一個棧區,棧中只儲存基礎資料型別的值和物件以及基礎資料的引用
2.每個棧中的資料(基礎資料型別和物件引用)都是私有的,其他棧不能訪問。
3.棧分為3個部分:基本型別變數區、執行環境上下文、操作指令區(存放操作指令)。

堆區:
1.儲存的全部是物件,每個物件都包含一個與之對應的class的資訊。(class的目的是得到操作指令)
2.jvm只有一個堆區(heap)被所有執行緒共享,堆中不存放基本型別和物件引用,只存放物件本身 。

比如主函式裡的語句 int [] arr=new int [3];在記憶體中是怎麼被定義的:

主函式先進棧,在棧中定義一個變數arr,接下來為arr賦值,但是右邊不是一個具體值,是一個實體。實體建立在堆裡,在堆裡首先通過new關鍵字開闢一個空間,記憶體在儲存資料的時候都是通過地址來體現的,地址是一塊連續的二進位制,然後給這個實體分配一個記憶體地址。陣列都是有一個索引,陣列這個實體在堆記憶體中產生之後每一個空間都會進行預設的初始化(這是堆記憶體的特點,未初始化的資料是不能用的,但在堆裡是可以用的,因為初始化過了,但是在棧裡沒有初始化這一功能),不同的型別初始化的值不一樣。所以堆和棧裡就建立了實體(物件)和變數:

那麼堆和棧是怎麼聯絡起來的呢?

我們剛剛說過給堆分配了一個地址,把堆的地址賦給arr,arr就通過地址指向了陣列。所以arr想操縱陣列時,就通過地址,而不是直接把實體都賦給它。這種我們不再叫他基本資料型別,而叫引用資料型別。稱為arr引用了堆記憶體當中的實體。(可以理解為c或c++的指標,Java成長自c++和c++很像,優化了c++)

如果當int [] arr=null;

arr不做任何指向,null的作用就是取消引用資料型別的指向。

當一個實體,沒有引用資料型別指向的時候,它在堆記憶體中不會被釋放,而被當做一個垃圾,在不定時的時間內自動回收,因為Java有一個自動回收機制,(而c++沒有,需要程式設計師手動回收,如果不回收就越堆越多,直到撐滿記憶體溢位,所以Java在記憶體管理上優於c++)。自動回收機制(程式)自動監測堆裡是否有垃圾,如果有,就會自動的做垃圾回收的動作,但是什麼時候收不一定。

所以堆與棧的區別很明顯:

1.棧記憶體儲存的是區域性變數而堆記憶體儲存的是實體;

2.棧記憶體的更新速度要快於堆記憶體,因為區域性變數的生命週期很短;

3.棧記憶體存放的變數生命週期一旦結束就會被釋放,而堆記憶體存放的實體會被垃圾回收機制不定時的回收。

字串在記憶體中的儲存

用以下程式碼做分析展示:

 public static void main(String[] args) {
        String s1 = "abc";  
        String s2 = "abc";
        String s3 = "xxx";
    }

檢視其編譯後的 class 檔案如下:

用圖解的方式展示:

String s1 = "abc";resolve 過程在字串常量池中發現沒有”abc“的引用,便在堆中新建一個”abc“的物件,並將該物件的引用存入到字串常量池中,然後把這個引用返回給 s1。

String s2 = "abc";resolve 過程會發現 StringTable 中已經有了”abc“物件的引用,則直接返回該引用給 s2,並不會建立任何物件。

String s3 = "xxx";同第一行程式碼一樣,在堆中建立物件,並將該物件的引用存入到 StringTable,最後返回引用給 s3。

再看:

public static void main(String[] args) {
        String s1 = "ab";//#1
        String s2 = new String(s1+"d");//#2
        s2.intern();//#3
        String s4 = "xxx";//#4
        String s3 = "abd";//#5
        System.out.println(s2 == s3);//true
    }

檢視其編譯後的 class 檔案如下:

通過 class 檔案資訊可知,“ab”、“d”、“xxx”,“abd”進入到了 class 檔案常量池,由於類在 resolve 階段是 lazy 的,所以是不會建立例項物件,更不會駐留字串常量池。

圖解如下:

進入 main 方法,對每行程式碼進行解讀。

  • 1,ldc 指令會把“ab”載入到棧頂,換句話說,在堆中建立“ab”物件,並把該物件的引用儲存到字串常量池中。
  • 2,ldc 指令會把“d”載入到棧頂,然後有個拼接操作,內部是建立了一個 StringBuilder 物件,一路 append,最後呼叫 StringBuilder 物件的 toString 方法得到一個 String 物件(內容是 abd,注意 toString 方法會 new 一個 String 物件),並把它賦值給 s2(賦值給 s2 的依然是物件的引用而已)。注意此時沒有把“abd”物件的引用放入字串常量池。
  • 3,intern 方法首先會去字串常量池中查詢是否有“abd”物件的引用,如果沒有,則把堆中“abd”物件的引用儲存到字串常量池中,並返回該引用,但是我們並沒有使用變數去接收它。
  • 4,無意義,只是為了說明 class 檔案中的“abd”字面量是#5時得到的。
  • 5,字串常量池中已經有“abd”物件的引用,因此直接將該引用返回給 s3。

字串常量池

  • 程式中直接用雙引號寫的字串,儲存至字串常量池中,即只有下圖第四種建立方式的字串是儲存至字串常量池中(只要寫了new,就不在常量池了)。
  • 對於基本資料型別,== 是進行數值的比較,
    對於引用資料型別,==是進行地址值的比較
    如下圖所示:

String str = new String("abc");問一共創造了幾個物件???

String str=new String("abc"); 緊接著這段程式碼之後的往往是這個問題,那就是這行程式碼究竟建立了幾個String物件呢?

相信大家對這道題並不陌生,答案也是眾所周知的,2個。

接下來我們就從這道題展開,一起回顧一下與建立String物件相關的一些JAVA知識。

我們可以把上面這行程式碼分成String str、=、"abc"和new String()四部分來看待。String str只是定義了一個名為str的String型別的變數,因此它並沒有建立物件;=是對變數str進行初始化,將某個物件的引用(或者叫控制代碼)賦值給它,顯然也沒有建立物件;現在只剩下new String("abc")了。那麼,new String("abc")為什麼又能被看成"abc"和new String()呢?

我們來看一下被我們呼叫了的String的構造器:

public String(String original) { //other code ... } 大家都知道,我們常用的建立一個類的例項(物件)的方法有以下兩種:

一、使用new建立物件。

二、呼叫Class類的newInstance方法,利用反射機制建立物件

我們正是使用new呼叫了String類的上面那個構造器方法建立了一個物件,並將它的引用賦值給了str變數。同時我們注意到,被呼叫的構造器方法接受的引數也是一個String物件,這個物件正是"abc"。由此我們又要引入另外一種建立String物件的方式的討論——引號內包含文字。

這種方式是String特有的,並且它與new的方式存在很大區別。

String str="abc";

毫無疑問,這行程式碼建立了一個String物件。

String a="abc"; String b="abc"; 那這裡呢?

答案還是一個。

String a="ab"+"cd"; 再看看這裡呢?

答案是三個。

說到這裡,我們就需要引入對字串池相關知識的回顧了。

在JAVA虛擬機器(JVM)中存在著一個字串池,其中儲存著很多String物件,並且可以被共享使用,因此它提高了效率。由於String類是final的,它的值一經建立就不可改變,因此我們不用擔心String物件共享而帶來程式的混亂。字串池由String類維護,我們可以呼叫intern()方法來訪問字串池。

我們再回頭看看String a="abc";,這行程式碼被執行的時候,JAVA虛擬機器首先在字串池中查詢是否已經存在了值為"abc"的這麼一個物件,它的判斷依據是String類equals(Object obj)方法的返回值。如果有,則不再建立新的物件,直接返回已存在物件的引用;如果沒有,則先建立這個物件,然後把它加入到字串池中,再將它的引用返回。因此,我們不難理解前面三個例子中頭兩個例子為什麼是這個答案了。

只有使用引號包含文字的方式建立的String物件之間使用“+”連線產生的新物件才會被加入字串池中。對於所有包含new方式新建物件(包括null)的“+”連線表示式,它所產生的新物件都不會被加入字串池中,對此我們不再贅述。因此我們提倡大家用引號包含文字的方式來建立String物件以提高效率,實際上這也是我們在程式設計中常採用的。



方法區:
1.又叫靜態區,跟堆一樣,被所有的執行緒共享。方法區包含所有的class和static變數。
2.方法區中包含的都是在整個程式中永遠唯一的元素,如class,static變數。