Java中棧和堆講解

之前對JVM中堆記憶體和棧記憶體都是一直半解,今天有空就好好整理一下,用作學習筆記。   

包括Java程式在內,任何程式在運行時都是要開闢記憶體空間的。JVM運行時在記憶體中開闢一片記憶體區域,啟動時在自己的記憶體區域中進行更細緻的劃分,因為虛擬機中每一片記憶體處理的方式都不同,所以要單獨進行管理。實際上在JVM有五種記憶體管理形式:

  1. 暫存器;
  2. 本地方法區;
  3. 方法區;
  4. 棧記憶體(stack);
  5. 堆記憶體(heap);

    今天重點梳理一下棧記憶體和堆記憶體。

     在講解之前我們要了解一個電腦發展至今仍然無法解決的一個矛盾,就是記憶體的存取速度和數據大小之間的矛盾。當我們對存取速度越快,存儲的數據量就越少,反之亦然。棧記憶體、堆記憶體其實就是對這種矛盾的一種妥協方式,它們有自己的優點也有自己的缺點:

  • 棧記憶體:存取速度要比堆記憶體快,僅次於CPU中的暫存器,但棧記憶體中數據大小和周期時固定的。
  • 堆記憶體:可以動態地分配記憶體大小,但存取速度慢。

     那麼棧記憶體、堆記憶體到底存儲那些數據呢?

  • 棧記憶體中存儲都是局部變數,棧中數據生存空間一般在當前scopes內(可以簡單理解為{…}括起來的區域)包含所有的基本類型(int、bool、char、float、double、short、long、byte)和引用類型。
  • 堆記憶體存儲時類的對象,即類的實體,凡是new建立的都是在堆中,堆中存放的都是實體(對象),實體用於封裝數據,而且是封裝多個(實體的多個屬性)。

    另外,在舉例前我們需要了解一個概念,什麼是變數?變數是記憶體中分配的區域的名稱。換句話說就是變數其實分配地址的別稱,我們通過這個變數的名字就可以找到一個指向這個變數所引用的數據的記憶體指針。我們知道了變數的類型,也就知道了這個指針地址後面連續幾個位元組記憶體儲的數據。

    我們以int[] arr=new int[]{1,2,3}為例,它的記憶體分配如下:

 

 從上圖我們可以看到,「變數」是存在棧記憶體中,「變數所指向的數據」是存在堆記憶體中的。

   下面我們舉一個更為複雜的類, 來展示每一部分到底是怎麼存儲的:

 

package class1;

class Fruit{
    static int x=10;
    static BigWaterMelon bigWaterMelon_1=new BigWaterMelon(x);
    
    int y=20;
    BigWaterMelon bigWaterMelon_2=new BigWaterMelon(y);
    
    public static void main(String[] args){
        final Fruit fruit=new Fruit();
        int z=30;
        BigWaterMelon bigWaterMelon_3=new BigWaterMelon(z);    
        new Thread(){
            @Override
            public void run(){
                int k=100;
                setWeight(k);
            }
            
            void setWeight(int waterMelonWeight){
                fruit.bigWaterMelon_2.Weight=waterMelonWeight;
            }
        }.start();
    }
    
}

class BigWaterMelon{
    public int Weight;
    public BigWaterMelon(int Weight){
        this.Weight=Weight;
    }
}

 

記憶體圖如下:

 

同一種顏色代表變數和對象的引用關係

由於方法區和堆記憶體的數據都是執行緒間共享的,所以執行緒Main Thread,New Thread和Another Thread都可以訪問方法區中的靜態變數以及訪問這個變數所引用的對象的實例變數。

棧記憶體中每個執行緒都有自己的虛擬機棧,每一個棧幀之間的數據就是執行緒獨有的了,也就是說執行緒New Thread中setWeight方法是不能訪問執行緒Main Thread中的局部變數bigWaterMelon_3,但是我們發現setWeight卻訪問了同為Main Thread局部變數的「fruit」,這是為什麼呢?因為「fruit」被聲明為final了。

當「fruit」被聲明為final後,「fruit」會作為New Thread的構造函數的一個參數傳入New Thread,也就是堆記憶體中Fruit$1對象中的實例變數val$fruit會引用「fruit」引用的對象,從而New Thread可以訪問到Main Thread的局部變數「fruit」。

 

此外,棧記憶體有先進後出(Last in first Out)的特點,並且棧中數據生存空間一般在當前scopes內(可以簡單理解為{…}括起來的區域),也就是說當方法執行結束後,方法內的局部變數在記憶體中就被清除了。但堆記憶體不會自動清除,它回不斷地申請新的堆記憶體地址來存儲新的數據。不再使用地舊數據只會當作「垃圾數據」,在C++中需要你手動清除,在JVM會自動將這些垃圾數據回收,也就是傳說中地GC。

無論是棧記憶體還是堆記憶體,記憶體空間都是有限的。當堆記憶體沒有可用空間時,比如遞歸沒有跳出,JVM會拋出java.lang.StackOverFlowError;當堆記憶體沒有空間時,比如在while循環中不斷創建實例,JVM會拋出java.lang.OutOfMemoryError。

——————————————————

參考博文://www.cnblogs.com/pomodoro/p/11912025.html

參考博文://blog.csdn.net/jianghao233/article/details/82777789