1. 程式人生 > >Java記憶體圖以及堆、棧、常量區、靜態區、方法區的區別

Java記憶體圖以及堆、棧、常量區、靜態區、方法區的區別

這裡寫圖片描述
如果是一個類裡面的靜態成員變數和靜態成員方法,它是儲存在方法區的,靜態成員變數是在方法區的靜態域裡面,而靜態成員方法是在方法區的class二進位制資訊裡面(.class檔案和方法區裡面的二進位制資訊不一樣,讀取.class檔案按照虛擬機器需要的格式儲存在方法區。這種格式包括資料結構方面),靜態成員和靜態成員方法使用時不用建立物件,即類載入初始化後就可以使用,並且是執行緒共享的。

通過圖中分析,很多問題也能夠迎刃而解,比如不同執行緒呼叫方法為什麼是執行緒安全的。區域性變數儲存在哪兒裡(棧中),成員變數儲存在哪兒裡(靜態成員變數儲存在方法區,非靜態成員變數儲存在堆區),為什麼區域性變數不能夠static修飾等(區域性變數儲存在棧區,在方法呼叫時不能夠自動初始化必須由程式設計師手動初始化,否則會報錯,歸根結底是由於static變數和區域性變數儲存的位置不一樣。)。

Java記憶體空間個人理解

堆:堆主要存放Java在執行過程中new出來的物件,凡是通過new生成的物件都存放在堆中,對於堆中的物件生命週期的管理由Java虛擬機器的垃圾回收機制GC進行回收和統一管理。類的非靜態成員變數也放在堆區,其中基本資料型別是直接儲存值,而複雜型別是儲存指向物件的引用,非靜態成員變數在類的例項化時開闢空間並且初始化。所以你要知道類的幾個時機,載入-連線-初始化-例項化。詳情請檢視部落格:Java中 類的載入概述和載入時機

棧:棧主要存放在執行期間用到的一些區域性變數(基本資料型別的變數)或者是指向其他物件的一些引用,因為方法執行時,被分配的記憶體就在棧中,所以當然儲存的區域性變數就在棧中咯。當一段程式碼或者一個方法呼叫完畢後,棧中為這段程式碼所提供的基本資料型別或者物件的引用立即被釋放;

常量池:常量池是方法區的一部分記憶體。常量池在編譯期間就將一部分資料存放於該區域,包含基本資料型別如int、long等以final宣告的常量值,和String字串、特別注意的是對於方法執行期位於棧中的區域性變數String常量的值可以通過 String.intern()方法將該值置入到常量池中。

靜態域:位於方法區的一塊記憶體。存放類中以static宣告的靜態成員變數

方法區:是各個執行緒共享的記憶體區域,它用於儲存class二進位制檔案,包含了虛擬機器載入的類資訊、常量、靜態變數、即時編譯後的程式碼等資料。它有個名字叫做Non-Heap(非堆),目的是與Java堆區分開。

需要特別注意的是:

方法區是執行緒安全的。由於所有的執行緒都共享方法區,所以,方法區裡的資料訪問必須被設計成執行緒安全的。例如,假如同時有兩個執行緒都企圖訪問方法區中的同一個類,而這個類還沒有被裝入JVM,那麼只允許一個執行緒去裝載它,而其它執行緒必須等待 !

最後總結起來就是:

棧:為即時呼叫的方法開闢空間,儲存區域性變數值(基本資料型別),區域性變數引用。注意:區域性變數必須手動初始化。
堆:存放引用型別的物件,即new出來的物件、陣列值、類的非靜態成員變數值(基本資料型別)、非靜態成員變數引用。其中非靜態成員變數在例項化時開闢空間初始化值。更具體點,個人感覺非靜態成員變數是放在堆的物件中。
方法區:存放class二進位制檔案。包含類資訊、靜態變數,常量池(String字串和final修飾的常量值等),類的版本號等基本資訊。因為是共享的區域,所以如果靜態成員變數的值或者常量值(String型別的值能夠非修改,具體請檢視部落格)被修改了直接就會反應到其它類的物件中。

成員變數與區域性變數總結:

一:在方法中宣告的變,即該變數是區域性變數,每當程式呼叫方法時,系統都會為該方法建立一個方法棧,其所在方法中宣告的變數就放在方法棧中,當方法結束系統會釋放方法棧,其對應在該方法中宣告的變數隨著棧的銷燬而結束,這就區域性變數只能在方法中有效的原因<1>在方法中生明的變數可以是基本型別的變數,也可以是引用型別的變數,(1)當宣告是基本型別的變數的時,其變數名及值(變數名及值是兩個概念)是放在方法棧中(2)當宣告的是引用變數時,所宣告的變數(該變數實際上是在方法中儲存的是記憶體地址值)是放在方法的棧中,該變數所指向的物件是放在堆類存中的》》》二:在類中宣告的變數是成員變數,也叫全域性變數,放在堆中的,<1>同樣在類中宣告的變數即可是基本型別的變數 也可是引用型別的變數(1)當宣告的是基本型別的變數其變數名及其只時放在堆類存中的,(2)引用型別時,其宣告的變數仍然會儲存一個記憶體地址值,該記憶體地址值指向所引用的物件

下面給大家看一個Java程式碼例子:

宣告一個類:

public class A {
    public final String tempString="world";//這裡可以把final去掉,結果等同!!
    public final char[] charArray="Hello".toCharArray();
    public char[] getCharArray() {
        return charArray;
    }
    public String getTempString() {
        return tempString;
    }
}

建立測試類:

public class TestA {
    public static void main(String[] args) {
        A a1=new  A();
        A a2=new A();

        System.out.println(a1.charArray==a2.charArray);
        System.out.println(a1.tempString==a2.tempString);
    }
}

輸出結果:

false
true

要想明白上面字串對比為什麼輸出為true你必須知道:

這裡寫圖片描述

該圖片截自《深入理解Java虛擬機器》

  • 一個Class位元組碼檔案的Class位元組碼檔案物件只有一個常量池,常量池被所有執行緒共享。
  • 在常量池中,字串被儲存為一個字元序列,每個字元序列都對應一個String物件,該物件儲存在堆中。所以也就是說為什麼String temp="xxx";能夠當成一個物件使用!!
  • 如果多個執行緒去訪問A類中的String字串,每次都會到常量區中去找該字元序列的引用。
  • 所以訪問A類被建立的兩個A型別物件的String字串對比會輸出true。

那麼為什麼final型別的字元陣列就不為true了呢??

申明(不管是通過new還是通過直接寫一個數組)一個數組其實在Java中就等同建立了一個物件,即每次建立類的物件都會自動建立一個新的陣列空間。

其中要注意的是:常量池中儲存字元陣列只是儲存的是每個字元或者字串。

為了證明每次獲取的final陣列地址不一樣,並且陣列中的字元都會儲存在常量池中,我們需要參考另外一個程式碼:

public class A {
    public String tempString="world";
    public final String tempStringArray[]={"Fire","Lang"};
    public final char[] charArray={'h','e','l','l','o'};
    public Character charx='l';

    public char[] getCharArray() {
        return charArray;
    }
    public String getTempString() {
        return tempString;
    }
    public String[] getTempStringArray() {
        return tempStringArray;
    }
    public Character getCharx() {
        return charx;
    }
}

測試程式碼:

public class TestA {
    public static void main(String[] args) {
        A a1=new  A();
        A a2=new A();
        System.out.println(a1.tempString==a2.tempString);
        System.out.println(a1.tempStringArray==a2.tempStringArray);//看這裡
        System.out.println("#####################");//看這裡
        System.out.println(a1.tempStringArray[0]==a2.tempStringArray[0]);
        System.out.println(a1.tempStringArray[0]=="Fire");
        System.out.println("#####################");
        System.out.println(a1.charArray==a2.charArray);
        System.out.println(a1.charx==a2.charx);
    }
}

輸出:

true
false
#####################
true
true
#####################
false
true

可以看到每次輸出的final陣列地址都不一樣,最重要的是String型別的陣列地址也都不一樣!!但是String型別陣列中的每個字串都儲存在常量池中。

所以可以肯定的是字串和其它能夠確定值的final字面量值是儲存在常量池的!!並且在方法區記憶體中只有一份!!與所有執行緒共享訪問!!

常量池儲存的專案型別:

這裡寫圖片描述

參考連結: