1. 程式人生 > 其它 >Java 類和物件在記憶體中的表現形式,棧、堆、方法區、常量池

Java 類和物件在記憶體中的表現形式,棧、堆、方法區、常量池

Java記憶體分配與管理是Java的核心技術之一,不管學習任何一門語言,我們要知其然,知其所以然,本文主要分析下Java中類和物件在記憶體中的表現形式,方便我們對其有更深瞭解。一般Java在記憶體分配時會涉及到這幾個區域:棧區(stack)、堆區(heap)、方法區(Method Area)、常量池。我們先對下面幾個概念進行深刻了解後,再進行畫圖分析類和物件在記憶體中的變化及表現形式。

棧:存放基本型別的資料和物件的引用變數的資料,但物件本身不存放在棧中,而是存放在堆中(new 出來的物件)

堆:存放用new產生的物件資料,每個物件包含了一個與之對應的 class 類的資訊。

方法區(又稱為靜態區):存放物件中用static定義的靜態成員

常量池:通常用來存放常量資料、靜態變數、類的載入資訊等

一、棧區

在函式(方法)中定義的一些基本型別的變數或者物件的引用變數都在棧記憶體中分配。

當在一段程式碼塊定義一個變數時,Java就在棧中為這個變數分配記憶體空間,當該變數退出該作用域後,Java會自動釋放掉為該變數所分配的記憶體空間,該記憶體空間可以立即被另作他用。棧中的資料大小和生命週期是可以確定的,當沒有引用指向資料時,這個資料就會消失。

每個方法(Method)執行時,都會建立一個方法棧區,用於儲存區域性變量表、運算元棧、動態連結、方法出口資訊等

棧中所儲存的變數和引用都是區域性的(即:定義在方法體中的變數或者引用),區域性變數和引用都在棧中(包括final的區域性變數)

八種基本資料型別(byte、short、int、long、float、double、char、boolean)的區域性變數(定義在方法體中的基本資料型別的變數)在棧中儲存的是它們對應的值

每個執行緒包含一個棧區,棧中只儲存基本資料型別的變數和引用資料型別的變數,每個棧中的資料(基本資料型別和物件的引用)都是私有的,其它棧是無法進行訪問的。棧分為3個部分:基本型別變數區、執行環境上下文、操作指令區(存放操作指令)。

棧中還儲存區域性的物件的引用(定義在方法體中的引用型別的變數),物件的引用並不是物件本身,而是物件在堆中的地址,換句話說,區域性的物件的引用所指物件在堆中的地址在儲存在了棧中。當然,如果物件的引用沒有指向具體的物件,物件的引用則是null

棧的優勢是,存取速度比堆要快,僅次於暫存器,棧資料可以共享。但缺點是,存在棧中的資料大小與生存期必須是確定的,缺乏靈活性。

棧有一個很重要的特殊性,就是存在棧中的資料可以共享。

二、堆區

堆記憶體用來存放由new建立的物件和陣列。在堆中分配的記憶體,由Java虛擬機器的自動垃圾回收器來管理。

堆記憶體是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。Java堆(Java Heap)唯一目的就是存放物件例項。所有的物件例項及陣列都要在**Java堆(Java Heap)**上分配記憶體空間。

在堆中產生了一個數組或物件後,在棧中定義一個特殊的變數,讓棧中這個變數的取值等於陣列或物件在堆記憶體中的首地址,棧中的這個變數就成了陣列或物件的引用變數。引用變數就相當於是為陣列或物件起的一個名稱,以後就可以在程式中使用棧中的引用變數來訪問堆中的陣列或物件。引用變數就相當於是為陣列或者物件起的一個名稱。

引用變數是普通的變數,定義時在棧中分配,引用變數在程式執行到其作用域之外後被釋放。而陣列和物件本身在堆中分配,即使程式執行到使用 new 產生陣列或者物件的語句所在的程式碼塊之外,陣列和物件本身佔據的記憶體不會被釋放,陣列和物件在沒有引用變數指向它的時候,才變為垃圾,不能在被使用,但仍然佔據記憶體空間不放,在隨後的一個不確定的時間被垃圾回收器收走(釋放掉),這也是Java比較佔記憶體的原因。

實際上,棧中的變數指向堆記憶體中的變數,這就是Java中的指標!

Java的堆是一個執行時資料區,類的物件從中分配空間。物件一般通過new 來建立,例如new Date(),它們不需要程式程式碼來顯式的釋放。堆是由垃圾回收來負責的,堆的優勢是可以動態地分配記憶體大小,生存期也不必事先告訴編譯器,因為它是在執行時動態分配記憶體的,Java的垃圾收集器會自動收走這些不再使用的資料。但缺點是,由於要在執行時動態分配記憶體,存取速度較慢。

三、方法區

方法區跟堆一樣,又被稱為靜態區,通常存放常量資料。它儲存已被Java虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等,它跟堆一樣,被所有的執行緒共享。

3.1 儲存的類資訊

對每個載入的型別(類class、介面interface、列舉enum、註解annotation),JVM必須在方法區中儲存以下型別資訊:

  • 這個型別的完整有效名稱(全名=包名.類名)

  • 這個型別直接父類的完整有效名稱( java.lang.Object除外,其他型別若沒有宣告父類,預設父類是Object)

  • 這個型別的修飾符(public、abstract、final的某個子集)

  • 這個型別直接介面的一個有序列表

除此之外還方法區(Method Area)儲存類資訊還有

  • 型別的常量池( constant pool)

  • 域(Field)資訊

  • 方法(Method)資訊

  • 除了常量外的所有靜態(static)變數

3.2 儲存的常量

static final修飾的成員變數都儲存於 方法區(Method Area)中

3.3 儲存的靜態變數

  • 靜態變數又稱為類變數,類中被static修飾的成員變數都是靜態變數(類變數)

  • 靜態變數之所以又稱為類變數,是因為靜態變數和類關聯在一起,隨著類的載入而存在於方法區(而不是堆中)

  • 八種基本資料型別(byte、short、int、long、float、double、char、boolean)的靜態變數會在方法區開闢空間,並將對應的值儲存在方法方法區,對於引用型別的靜態變數如果未用new關鍵字為引用型別的靜態變數分配物件(如:static Object obj;),那麼物件的引用obj會儲存在方法區中,併為其指定預設值null;若對於引用型別的靜態變數如果用new關鍵字為引用型別的靜態變數分配物件(如:static Cat cat = new Cat();),那麼物件的引用cat會儲存在方法區中,並且該物件在堆中的地址也會儲存在方法區中(注意此時靜態變數只儲存了物件的堆地址,而物件本身仍在堆記憶體中);當然這個過程還涉及到靜態變數初始化問題。

3.4 儲存的方法(Method)

程式執行時會載入類編譯生成的位元組碼,這個過程中靜態變數(類變數)和靜態方法及普通方法對應的位元組碼載入到方法區。

方法區中沒有例項變數,這是因為,類載入先於對應類物件的產生,而例項變數是和物件關聯在一起的,沒有物件就不存在例項變數,類載入時沒有物件,所以方法區中沒有例項變數。

靜態變數(類變數)和靜態方法及普通方法在方法區(Method Area)儲存方式是有區別的

四、常量池

常量池指的是在編譯期被確定,並被儲存在已編譯的.class檔案中的一些資料。

除了包含程式碼中所定義的各種基本型別(如int、long等等)和物件型(如String及陣列)的常量值(final)還包含一些以文字形式出現的符號引用,比如:類和介面的全限定名;欄位的名稱和描述符;方法和名稱和描述符。

虛擬機器必須為每個被裝載的型別維護一個常量池。常量池就是該型別所用到常量的一個有序集和,包括直接常量(string,integer和floating point常量)和對其他型別,欄位和方法的符號引用。對於String常量,它的值是在常量池中的。而JVM中的常量池在記憶體當中是以表的形式存在的,對於String型別,有一張固定長度的CONSTANT_String_info表用來儲存文字字串值,注意:該表只儲存文字字串值,不儲存符號引用。說到這裡,對常量池中的字串值的儲存位置應該有一個比較明瞭的理解了。在程式執行的時候,常量池會儲存在方法區(Method Area),而不是堆中。

五、畫圖分析類例項化及操作時在記憶體中的變化及表現形式

package com.joshua317;

public class Main {

    public static void main(String[] args) {
        //例項化一個Cat物件
        Cat cat = new Cat();
        //給成員變數賦值
        cat.name = "招財";
        cat.age = 2;
        cat.weight = 2.02;
        //列印
        System.out.println("小貓的名字:"+cat.name + " 小貓的年齡:"+cat.age);
        //呼叫成員方法
        cat.say();
    }
}

class Cat {
    /**
     * 成員變數 name
     */
    String name;
    /**
     * 成員變數 age
     */
    int age;
    /**
     * 成員變數 weight
     */
    double weight;

    public void say()
    {
        System.out.println("喵喵~~");
    }
}
Java

上面這段程式碼首先有個主程式的類Main,這個我們不過多說明。我們主要分析main函式體裡面的這段程式碼。

我們需要知道在Cat類中,定義了三個成員屬性:name、age、weight;定義了一個成員方法:say();

//例項化一個Cat物件
Cat cat = new Cat();
//給成員變數賦值
cat.name = "招財";
cat.age = 2;
cat.weight = 2.02;
//列印
System.out.println("小貓的名字:"+cat.name + " 小貓的年齡:"+cat.age);
//呼叫成員方法
cat.say();

在main() 函式裡實例化物件 cat, 記憶體中在堆區內會給例項化物件 cat 分配一個記憶體地址,然後我們給物件 cat進行了賦值並且列印了一些資訊,最後呼叫了成員方法 say() ,程式執行完畢。

1.在程式的執行過程中,首先Main類中的成員屬性和成員方法會載入到方法區

2.程式執行類Main的main() 方法時,main()函式方法體會進入棧區,這一過程叫做進棧(壓棧)。

3.程式執行到Cat cat = new Cat();時,首先會把Cat類的成員屬性和成員方法載入到方法區,此時方法的記憶體空間地址為1x000000,同時在在堆記憶體開闢一塊記憶體空間74a14482,用於存放 Cat 例項物件,並給成員屬性及成員方法分配對應的地址空間,比如下圖的0x000001~0x000004即為物件分配的堆記憶體地址,但此時成員屬性都是預設值,比如int型別預設值為0,String型別預設值為null,成員方法地址值為方法區對應成員方法體的記憶體地址值;然後在棧記憶體中會給變數cat分配一個棧地址34b23231,用來存放Cat例項物件的引用地址的值74a14482

4.接下來對 cat 物件進行賦值

//給成員變數賦值
cat.name = "招財";
cat.age = 2;
cat.weight = 2.02;

先在棧區找到引用變數cat,然後根據地址值找到 new Cat() 物件的記憶體地址,並對裡面的屬性進行賦值操作。由於成員屬性name的型別為String,為引用資料型別,所以此時會在常量池開闢一塊地址空間2x00000000,存放招財這個值,而age的型別為int,weight的型別為double,都為基本資料型別,所以值直接存放堆中。

5.當程式執行到cat.say() ;方法時,會先到棧區找到cat這個引用變數(這個變數存的是物件的引用地址),然後根據該地址值在堆記憶體中找到 new Cat() 物件裡面的say()方法進行呼叫,在呼叫say()方法時,會在棧區開闢一塊空間進行執行。

6.在方法體void say()被呼叫完成後,就會立刻馬上從棧內彈出(出站 ),最後,在main()函式完成後,main()函式也會出棧