1. 程式人生 > >java程式執行時記憶體分配詳解

java程式執行時記憶體分配詳解

一、 基本概念

   每執行一個java程式會產生一個java程序,每個java程序可能包含一個或者多個執行緒,每一個Java程序對應唯一一個JVM例項,每一個JVM例項唯一對應一個堆,每一個執行緒有一個自己私有的棧。程序所建立的所有類的例項(也就是物件)或陣列(指的是陣列的本身,不是引用)都放在堆中,並由該程序所有的執行緒共享。Java中分配堆記憶體是自動初始化的,即為一個物件分配記憶體的時候,會初始化這個物件中變數。雖然Java中所有物件的儲存空間都是在堆中分配的,但是這個物件的引用卻是在棧中分配,也就是說在建立一個物件時在堆和棧中都分配記憶體,在堆中分配的記憶體實際存放這個被建立的物件的本身,而在棧中分配的記憶體只是存放指向這個堆物件的引用而已。區域性變數 new 出來時,在棧空間和堆空間中分配空間,當局部變數生命週期結束後,棧空間立刻被回收,堆空間區域等待GC回收。

具體的概念:JVM的記憶體可分為3個區:堆(heap)、棧(stack)和方法區(method,也叫靜態區):

堆區: 

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

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

方法區(靜態區): 


1.被所有的執行緒共享,方法區包含所有的class(class是指類的原始程式碼,要建立一個類的物件,首先要把該類的程式碼載入到方法區中,並且初始化)和static變數。 
2.方法區中包含的都是在整個程式中永遠唯一的元素,如class,static變數。 
 

二、例項演示

複製程式碼
 1   AppMain.java 
 2  3   public   class  AppMain                //執行時, jvm 把appmain的程式碼全部都放入方法區     
 4   {     
 5   public   static   void  main(String[] args)  //
main 方法本身放入方法區。     6   {     7   Sample test1 = new  Sample( " 測試1 " );   //test1是引用,所以放到棧區裡, Sample是自定義物件應該放到堆裡面     8   Sample test2 = new  Sample( " 測試2 " );     9 10   test1.printName();     11   test2.printName();     12   }     13   }     14 15   public   class  Sample        //執行時, jvm 把appmain的資訊都放入方法區     16   {     17   /** 範例名稱 */18   private String name;      //new Sample例項後, name 引用放入棧區裡, name 對應的 String 物件放入堆裡     19 20   /** 構造方法 */21   public  Sample(String name)     22   {     23   this .name = name;     24   }     25 26   /** 輸出 */27   public   void  printName()   //在沒有物件的時候,print方法跟隨sample類被放入方法區裡。     28   {     29   System.out.println(name);     30   }     31   }   
複製程式碼

     執行該程式時,首先啟動一個Java虛擬機器程序,這個程序首先從classpath中找到AppMain.class檔案,讀取這個檔案中的二進位制資料,然後把Appmain類的類資訊存放到執行時資料區的方法區中,這就是AppMain類的載入過程。 

   接著,Java虛擬機器定位到方法區中AppMain類的Main()方法的位元組碼,開始執行它的指令。這個main()方法的第一條語句就是: 

  Sample test1=new Sample("測試1"); 

該語句的執行過程:
    1、 Java虛擬機器到方法區找到Sample類的型別資訊,沒有找到,因為Sample類還沒有載入到方法區(這裡可以看出,java中的內部類是單獨存在的,而且剛開始的時候不會跟隨包含類一起被載入,等到要用的時候才被載入)。Java虛擬機器立馬載入Sample類,把Sample類的型別資訊存放在方法區裡。 
    2、 Java虛擬機器首先在堆區中為一個新的Sample例項分配記憶體, 並在Sample例項的記憶體中存放一個方法區中存放Sample類的型別資訊的記憶體地址。
    3、 JVM的程序中,每個執行緒都會擁有一個方法呼叫棧,用來跟蹤執行緒執行中一系列的方法呼叫過程,棧中的每一個元素就被稱為棧幀,每當執行緒呼叫一個方法的時候就會向方法棧壓入一個新幀。這裡的幀用來儲存方法的引數、區域性變數和運算過程中的臨時資料。

    4、位於“=”前的Test1是一個在main()方法中定義的一個變數(一個Sample物件的引用),因此,它被會新增到了執行main()方法的主執行緒的JAVA方法呼叫棧中。而“=”將把這個test1變數指向堆區中的Sample例項。
    5、JVM在堆區裡繼續建立另一個Sample例項,並在main方法的方法呼叫棧中新增一個Test2變數,該變數指向堆區中剛才建立的Sample新例項。

    6、JVM依次執行它們的printName()方法。當JAVA虛擬機器執行test1.printName()方法時,JAVA虛擬機器根據區域性變數test1持有的引用,定位到堆區中的Sample例項,再根據Sample例項持有的引用,定位到方法去中Sample類的型別資訊,從而獲得printName()方法的位元組碼,接著執行printName()方法包含的指令,開始執行。 

三、辨析

 

在Java語言裡堆(heap)和棧(stack)裡的區別 :

    1. 棧(stack)與堆(heap)都是Java用來在Ram中存放資料的地方。與C++不同,Java自動管理棧和堆,程式設計師不能直接地設定棧或堆。 
  2. 棧的優勢是,存取速度比堆要快,僅次於直接位於CPU中的暫存器。但缺點是,存在棧中的資料大小與生存期必須是確定的,缺乏靈活性。另外,棧資料可以共享(詳見下面的介紹)。堆的優勢是可以動態地分配記憶體大小,生存期也不必事先告訴編譯器,Java的垃圾收集器會自動收走這些不再使用的資料。但缺點是,由於要在執行時動態分配記憶體,存取速度較慢。 

Java中的2種資料型別:

  一種是基本型別(primitive types), 共有8類,即int, short, long, byte, float, double, boolean, char(注意,並沒有string的基本型別)。這種型別的定義是通過諸如int a = 3; long b = 255L;的形式來定義的,稱為自動變數。自動變數存的是字面值,不是類的例項,即不是類的引用,這裡並沒有類的存在。如int a = 3; 這裡的a是一個指向int型別的引用,指向3這個字面值。這些字面值的資料,由於大小可知,生存期可知(這些字面值固定定義在某個程式塊裡面,程式塊退出後,欄位值就消失了),出於追求速度的原因,就存在於棧中。 

    棧有一個很重要的特性:存在棧中的資料可以共享。假設我們同時定義:  int a = 3;  int b = 3;  編譯器先處理int a = 3;首先它會在棧中建立一個變數為a的引用,然後查詢有沒有字面值為3的地址,如果沒找到,就開闢一個存放3這個字面值的地址,然後將a指向3的地址。接著處理int b = 3;在建立完b的引用變數後,由於在棧中已經有3這個字面值,便將b直接指向3的地址。這樣,就出現了a與b同時均指向3的情況。 

這種字面值的引用與類物件的引用不同。假定兩個類物件的引用同時指向一個物件,如果一個物件引用變數修改了這個物件的內部狀態,那麼另一個物件引用變數也即刻反映出這個變化。相反,通過字面值的引用來修改其值,不會導致另一個指向此字面值的引用的值也跟著改變的情況。如上例,我們定義完a與 b的值後,再令a=4;那麼,b不會等於4,還是等於3。在編譯器內部,遇到a=4;時,它就會重新搜尋棧中是否有4的字面值,如果沒有,重新開闢地址存放4的值;如果已經有了,則直接將a指向這個地址。因此a值的改變不會影響到b的值。 
  另一種是包裝類資料,如Integer, String, Double等將相應的基本資料型別包裝起來的類。這些類資料全部存在於堆中,Java用new()語句來顯示地告訴編譯器,在執行時才根據需要動態建立,因此比較靈活,但缺點是要佔用更多的時間。 

四、總結

      java記憶體分配條理還是很清楚的,如果要徹底搞懂,可以去查閱JVM相關的書籍。在java中,記憶體分配最讓人頭疼的是String物件,由於其特殊性,所以很多程式設計師容易搞混淆,下一篇文章再詳細講解。