天天看點

(轉)Java裡的堆(heap)棧(stack)和方法區(method)(精華帖,多讀讀)

[color=red][/color]<一> 

基礎資料類型直接在棧空間配置設定, 方法的形式參數,直接在棧空間配置設定,當方法調用完成後從棧空間回收。   引用資料類型,需要用new來建立,既在棧空間配置設定一個位址空間,又在堆空間配置設定對象的類變量 。 方法的引用參數,在棧空間配置設定一個位址空間,并指向堆空間的對象區,當方法調用完成後從棧空間回收。局部變量 new 出來時,在棧空間和堆空間中配置設定空間,當局部變量生命周期結束後,棧空間立刻被回收,堆空間區域等待GC回收。 方法調用時傳入的 literal 參數,先在棧空間配置設定,在方法調用完成後從棧空間配置設定。字元串常量在

DATA 區域配置設定 ,this 在堆空間配置設定 。數組既在棧空間配置設定數組名稱, 又在堆空間配置設定數組實際的大小! 

哦 對了,補充一下static在DATA區域配置設定。 

從Java的這種配置設定機制來看,堆棧又可以這樣了解:堆棧(Stack)是作業系統在建立某個程序時或者線程(在支援多線程的作業系統中是線程)為這個線程建立的存儲區域,該區域具有先進後出的特性。 

每一個Java應用都唯一對應一個JVM執行個體,每一個執行個體唯一對應一個堆。應用程式在運作中所建立的所有類執行個體或數組都放在這個堆中,并由應用所有的線程共享.跟C/C++不同,Java中配置設定堆記憶體是自動初始化的。Java中所有對象的存儲空間都是在堆中配置設定的,但是這個對象的引用卻是在堆棧中配置設定,也就是說在建立一個對象時從兩個地方都配置設定記憶體,在堆中配置設定的記憶體實際建立這個對象,而在堆棧中配置設定的記憶體隻是一個指向這個堆對象的指針(引用)而已。 

<二> 

這兩天看了一下深入淺出JVM這本書,推薦給進階的java程式員去看,對你了解JAVA的底層和運作機制有 

比較大的幫助。 

廢話不想講了.入主題: 

先了解具體的概念: 

JAVA的JVM的記憶體可分為3個區:堆(heap)、棧(stack)和方法區(method) 

堆區: 

1.存儲的全部是對象,每個對象都包含一個與之對應的class的資訊。(class的目的是得到操作指令) 

2.jvm隻有一個堆區(heap)被所有線程共享,堆中不存放基本類型和對象引用,隻存放對象本身 

棧區: 

1.每個線程包含一個棧區,棧中隻儲存基礎資料類型的對象和自定義對象的引用(不是對象),對象都存放在堆區中 

2.每個棧中的資料(原始類型和對象引用)都是私有的,其他棧不能通路。 

3.棧分為3個部分:基本類型變量區、執行環境上下文、操作指令區(存放操作指令)。 

方法區: 

1.又叫靜态區,跟堆一樣,被所有的線程共享。方法區包含所有的class和static變量。 

2.方法區中包含的都是在整個程式中永遠唯一的元素,如class,static變量。 

為了更清楚地搞明白發生在運作時資料區裡的黑幕,我們來準備2個小道具(2個非常簡單的小程式)。 

AppMain.java 

Java代碼  

(轉)Java裡的堆(heap)棧(stack)和方法區(method)(精華帖,多讀讀)

public   class  AppMain                //運作時, jvm 把appmain的資訊都放入方法區   

{   

public   static   void  main(String[] args)  //main 方法本身放入方法區。   

Sample test1 = new  Sample( " 測試1 " );   //test1是引用,是以放到棧區裡, Sample是自定義對象應該放到堆裡面   

Sample test2 = new  Sample( " 測試2 " );   

test1.printName();   

test2.printName();   

}   

} Sample.java   

public   class  Sample        //運作時, jvm 把appmain的資訊都放入方法區   

/** 範例名稱 */   

private  name;      //new Sample執行個體後, name 引用放入棧區裡,  name 對象放入堆裡   

/** 構造方法 */   

public  Sample(String name)   

this .name = name;   

/** 輸出 */   

public   void  printName()   //print方法本身放入 方法區裡。   

System.out.println(name);   

OK,讓我們開始行動吧,出發指令就是:“java AppMain”,包包裡帶好我們的行動向導圖,Let’s GO! 

(轉)Java裡的堆(heap)棧(stack)和方法區(method)(精華帖,多讀讀)

系統收到了我們發出的指令,啟動了一個Java虛拟機程序,這個程序首先從classpath中找到AppMain.class檔案,讀取這個檔案中的二進制資料,然後把Appmain類的類資訊存放到運作時資料區的方法區中。這一過程稱為AppMain類的加載過程。 

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

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

語句很簡單啦,就是讓java虛拟機建立一個Sample執行個體,并且呢,使引用變量test1引用這個執行個體。貌似小case一樁哦,就讓我們來跟蹤一下Java虛拟機,看看它究竟是怎麼來執行這個任務的: 

1、 Java虛拟機一看,不就是建立一個Sample執行個體嗎,簡單,于是就直奔方法區而去,先找到Sample類的類型資訊再說。結果呢,嘿嘿,沒找到@@,這會兒的方法區裡還沒有Sample類呢。可Java虛拟機也不是一根筋的笨蛋,于是,它發揚“自己動手,豐衣足食”的作風,立馬加載了Sample類,把Sample類的類型資訊存放在方法區裡。 

2、 好啦,資料找到了,下面就開始幹活啦。Java虛拟機做的第一件事情就是在堆區中為一個新的Sample執行個體配置設定記憶體, 這個Sample執行個體持有着指向方法區的Sample類的類型資訊的引用。這裡所說的引用,實際上指的是Sample類的類型資訊在方法區中的記憶體位址,其實,就是有點類似于C語言裡的指針啦~~,而這個位址呢,就存放了在Sample執行個體的資料區裡。 

3、 在JAVA虛拟機程序中,每個線程都會擁有一個方法調用棧,用來跟蹤線程運作中一系列的方法調用過程,棧中的每一個元素就被稱為棧幀,每當線程調用一個方法的時候就會向方法棧壓入一個新幀。這裡的幀用來存儲方法的參數、局部變量和運算過程中的臨時資料。OK,原理講完了,就讓我們來繼續我們的跟蹤行動!位于“=”前的Test1是一個在main()方法中定義的變量,可見,它是一個局部變量,是以,它被會添加到了執行main()方法的主線程的JAVA方法調用棧中。而“=”将把這個test1變量指向堆區中的Sample執行個體,也就是說,它持有指向Sample執行個體的引用。 

OK,到這裡為止呢,JAVA虛拟機就完成了這個簡單語句的執行任務。參考我們的行動向導圖,我們終于初步摸清了JAVA虛拟機的一點點底細了,COOL! 

接下來,JAVA虛拟機将繼續執行後續指令,在堆區裡繼續建立另一個Sample執行個體,然後依次執行它們的printName()方法。當JAVA虛拟機執行test1.printName()方法時,JAVA虛拟機根據局部變量test1持有的引用,定位到堆區中的Sample執行個體,再根據Sample執行個體持有的引用,定位到方法去中Sample類的類型資訊,進而獲得printName()方法的位元組碼,接着執行printName()方法包含的指令。 

<三> 

在windows中使用taskmanager檢視java程序使用的記憶體時,發現有時候會超過 -Xmx制定的記憶體大小, -Xmx指定的是java heap,java還要配置設定記憶體做其他的事情,包括為每個線程建立棧。 

VM的每個線程都有自己的棧空間,棧空間的大小限制vm的線程數量,太大了,實用的線程數減少,太小容易抛出java.lang.StackOverflowError異常。windows預設為1M,linux必須運作ulimit -s 2048。 

在C語言裡堆(heap)和棧(stack)裡的差別 

簡單的可以了解為: 

heap:是由malloc之類函數配置設定的空間所在地。位址是由低向高增長的。 

stack:是自動配置設定變量,以及函數調用的時候所使用的一些空間。位址是由高向低減少。 

一個由c/C++編譯的程式占用的記憶體分為以下幾個部分 

1、棧區(stack)— 由編譯器自動配置設定釋放 ,存放函數的參數值,局部變量的值等。其操作方式類似于資料結構中的棧。 

2、在Java語言裡堆(heap)和棧(stack)裡的差別 

    1. 棧(stack)與堆(heap)都是Java用來在Ram中存放資料的地方。與C++不同,Java自動管理棧和堆,程式員不能直接地設定棧或堆。 

  2. 棧的優勢是,存取速度比堆要快,僅次于直接位于CPU中的寄存器。但缺點是,存在棧中的資料大小與生存期必須是确定的,缺乏靈活性。另外,棧資料可以共享,詳見第3點。堆的優勢是可以動态地配置設定記憶體大小,生存期也不必事先告訴編譯器,Java的垃圾收集器會自動收走這些不再使用的資料。但缺點是,由于要在運作時動态配置設定記憶體,存取速度較慢。 

  3. Java中的資料類型有兩種。 

  一種是基本類型(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()語句來顯示地告訴編譯器,在運作時才根據需要動态建立,是以比較靈活,但缺點是要占用更多的時間。 

4.每個JVM的線程都有自己的私有的棧空間,随線程建立而建立,java的stack存放的是frames ,java的stack和c的不同,隻是存放本地變量,傳回值和調用方法,不允許直接push和pop frames ,因為frames 可能是有heap配置設定的,是以j為ava的stack配置設定的記憶體不需要是連續的。java的heap是所有線程共享的,堆存放所有 runtime data ,裡面是所有的對象執行個體和數組,heap是JVM啟動時建立。 

  5. String是一個特殊的包裝類資料。即可以用String str = new String("abc");的形式來建立,也可以用String str = "abc";的形式來建立(作為對比,在JDK 5.0之前,你從未見過Integer i = 3;的表達式,因為類與字面值是不能通用的,除了String。而在JDK 5.0中,這種表達式是可以的!因為編譯器在背景進行Integer i = new Integer(3)的轉換)。前者是規範的類的建立過程,即在Java中,一切都是對象,而對象是類的執行個體,全部通過new()的形式來建立。Java

中的有些類,如DateFormat類,可以通過該類的getInstance()方法來傳回一個新建立的類,似乎違反了此原則。其實不然。該類運用了單例模式來傳回類的執行個體,隻不過這個執行個體是在該類内部通過new()來建立的,而getInstance()向外部隐藏了此細節。那為什麼在String str = "abc";中,并沒有通過new()來建立執行個體,是不是違反了上述原則?其實沒有。 

  5. 關于String str = "abc"的内部工作。Java内部将此語句轉化為以下幾個步驟: 

  (1)先定義一個名為str的對String類的對象引用變量:String str; 

  (2)在棧中查找有沒有存放值為"abc"的位址,如果沒有,則開辟一個存放字面值為"abc"的位址,接着建立一個新的String類的對象o,并将o 的字元串值指向這個位址,而且在棧中這個位址旁邊記下這個引用的對象o。如果已經有了值為"abc"的位址,則查找對象o,并傳回o的位址。 

  (3)将str指向對象o的位址。 

  值得注意的是,一般String類中字元串值都是直接存值的。但像String str = "abc";這種場合下,其字元串值卻是儲存了一個指向存在棧中資料的引用! 

為了更好地說明這個問題,我們可以通過以下的幾個代碼進行驗證。 

  String str1 = "abc"; 

  String str2 = "abc"; 

  System.out.println(str1==str2);  //true 

  注意,我們這裡并不用str1.equals(str2);的方式,因為這将比較兩個字元串的值是否相等。==号,根據JDK的說明,隻有在兩個引用都指向了同一個對象時才傳回真值。而我們在這裡要看的是,str1與str2是否都指向了同一個對象。 

  結果說明,JVM建立了兩個引用str1和str2,但隻建立了一個對象,而且兩個引用都指向了這個對象。 

  我們再來更進一步,将以上代碼改成: 

  str1 = "bcd"; 

  System.out.println(str1 + "," + str2);  //bcd, abc 

  System.out.println(str1==str2);  //false 

  這就是說,指派的變化導緻了類對象引用的變化,str1指向了另外一個新對象!而str2仍舊指向原來的對象。上例中,當我們将str1的值改為"bcd"時,JVM發現在棧中沒有存放該值的位址,便開辟了這個位址,并建立了一個新的對象,其字元串的值指向這個位址。 

  事實上,String類被設計成為不可改變(immutable)的類。如果你要改變其值,可以,但JVM在運作時根據新值悄悄建立了一個新對象,然後将這個對象的位址傳回給原來類的引用。這個建立過程雖說是完全自動進行的,但它畢竟占用了更多的時間。在對時間要求比較敏感的環境中,會帶有一定的不良影響。 

  再修改原來代碼: 

  String str3 = str1; 

  System.out.println(str3);  //bcd 

  String str4 = "bcd"; 

  System.out.println(str1 == str4);  //true 

  str3 這個對象的引用直接指向str1所指向的對象(注意,str3并沒有建立新對象)。當str1改完其值後,再建立一個String的引用str4,并指向因str1修改值而建立的新的對象。可以發現,這回str4也沒有建立新的對象,進而再次實作棧中資料的共享。 

  我們再接着看以下的代碼。 

  String str1 = new String("abc"); 

  建立了兩個引用。建立了兩個對象。兩個引用分别指向不同的兩個對象。 

  String str2 = new String("abc"); 

  以上兩段代碼說明,隻要是用new()來建立對象的,都會在堆中建立,而且其字元串是單獨存值的,即使與棧中的資料相同,也不會與棧中的資料共享。 

  6. 資料類型包裝類的值不可修改。不僅僅是String類的值不可修改,所有的資料類型包裝類都不能更改其内部的值。 

  7. 結論與建議: 

  (1)我們在使用諸如String str = "abc";的格式定義類時,總是想當然地認為,我們建立了String類的對象str。擔心陷阱!對象可能并沒有被建立!唯一可以肯定的是,指向 String類的引用被建立了。至于這個引用到底是否指向了一個新的對象,必須根據上下文來考慮,除非你通過new()方法來顯要地建立一個新的對象。是以,更為準确的說法是,我們建立了一個指向String類的對象的引用變量str,這個對象引用變量指向了某個值為"abc"的String類。清醒地認識到這一點對排除程式中難以發現的bug是很有幫助的。 

  (2)使用String str = "abc";的方式,可以在一定程度上提高程式的運作速度,因為JVM會自動根據棧中資料的實際情況來決定是否有必要建立新對象。而對于String str = new String("abc");的代碼,則一概在堆中建立新對象,而不管其字元串值是否相等,是否有必要建立新對象,進而加重了程式的負擔。這個思想應該是享元模式的思想,但JDK的内部在這裡實作是否應用了這個模式,不得而知。 

  (3)當比較包裝類裡面的數值是否相等時,用equals()方法;當測試兩個包裝類的引用是否指向同一個對象時,用==。 

  (4)由于String類的immutable性質,當String變量需要經常變換其值時,應該考慮使用StringBuffer類,以提高程式效率。 

如果java不能成功配置設定heap的空間,将抛出OutOfMemoryError

補充:

  首先厘清楚Stack,Heap的中文翻譯:Stack—棧,Heap—堆。

        在中文裡,Stack可以翻譯為“堆棧”,是以我直接查找了計算機術語裡面堆和棧開頭的詞語:

       堆存儲: heapstorage    堆存儲配置設定: heapstorage allocation  堆存儲管理: heap storage management

        棧編址: stack addressing   棧變換:stack transformation  棧存儲器:stack memory  棧單元: stack cell

          接着,總結在Java裡面Heap和Stack分别存儲資料的不同。

<col>

      Heap(堆)

      Stack(棧)

 JVM中的功能

     記憶體資料區    

               記憶體指令區

 存儲資料

     對象執行個體(1)

 基本資料類型, 指令代碼,常量,對象的引用位址(2)

1. 儲存對象執行個體,實際上是儲存對象執行個體的屬性值,屬性的類型和對象本身的類型标記等,并不儲存對象的方法(方法是指令,儲存在stack中)。

   對象執行個體在heap中配置設定好以後,需要在stack中儲存一個4位元組的heap記憶體位址,用來定位該對象執行個體在heap中的位置,便于找到該對象執行個體。

2. 基本資料類型包括byte、int、char、long、float、double、boolean和short。

    函數方法屬于指令.

 =======================       

  引用網上廣泛流傳的“Java堆和棧的差別”裡面對堆和棧的介紹;

          "Java 的堆是一個運作時資料區,類的(對象從中配置設定空間。這些對象通過new、newarray、anewarray和multianewarray等指令建立,它們不需要程式代碼來顯式的釋放。堆是由垃圾回收來負責的,堆的優勢是可以動态地配置設定記憶體大小,生存期也不必事先告訴編譯器,因為它是在運作時動态配置設定記憶體的,Java的垃圾收集器會自動收走這些不再使用的資料。但缺點是,由于要在運作時動态配置設定記憶體,存取速度較慢。"

        “棧的優勢是,存取速度比堆要快,僅次于寄存器,棧資料可以共享。但缺點是,存在棧中的資料大小與生存期必須是确定的,缺乏靈活性。棧中主要存放一些基本類型的變量(,int, short, long, byte, float, double, boolean, char)和對象句柄。 ”

         可見,垃圾回收GC是針對堆Heap的,而棧因為本身是FILO - first in, last out. 先進後出,能夠自動釋放。 這樣就能明白到new建立的,都是放到堆Heap!