Java中棧和堆講解

Jerryoned發表於2020-06-30

之前對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。

------------------------------------------------------

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

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

 

相關文章