1. 程式人生 > >Java 方法區及堆、棧(轉了慢慢看)

Java 方法區及堆、棧(轉了慢慢看)

第一部分

轉載自http://www.cnblogs.com/hqji/p/6582365.html

1、java中的棧(stack)和堆(heap)是java在記憶體(ram)中存放資料的地方

2、堆區

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

     jvm只有一個heap區,被所有執行緒共享,不存放基本型別和物件引用,只存放物件本身。

     堆的優劣勢:堆的優勢是可以動態的分配記憶體大小,生存期也不必事先告訴編譯器,java的垃圾收集器會自動收取這些不在使用的數據,但缺點是,由於要在執行時動態分配記憶體,存取速度慢。

3、棧區

     每一個執行緒包含一個stack區,只儲存基本資料型別的物件和自定義物件的引用(不是物件),物件都存放在共享heap中;

     每個棧中的資料(基本資料型別和物件引用)都是私有的,其他棧不能訪問;

     棧分為3部分:基本型別變數區、執行環境上下文、操作指令區(存放操作指令)

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

      stack:是一個先進後出的資料結構,通常儲存方法中的引數,區域性變數。

      在java中,所有基本型別和引用型別都在stack中儲存,棧中資料的生存空間一般在當前scopes內

4、方法區

     1)、又叫靜態區,跟堆一樣,被所有的執行緒共享。方法區包含所有的class和static變數;

     2)、方法區中包含的都是在程式中永遠的唯一的元素

5、在JAVA中,有六個不同的地方可以儲存資料:
  1. 暫存器(register)。

這是最快的儲存區,因為它位於不同於其他儲存區的地方——處理器內部。但是暫存器的數量極其有限,所以暫存器由編譯器根據需求進行分配。你不能直接控制,也不能在程式中感覺到暫存器存在的任何跡象。


  2. 堆疊(stack)。

位於通用RAM中,但通過它的“堆疊指標”可以從處理器哪裡獲得支援。堆疊指標若向下移動,則分配新的記憶體;若向上移動,則釋放那些 記憶體。這是一種快速有效的分配儲存方法,僅次於暫存器。建立程式時候,JAVA編譯器必須知道儲存

在堆疊內所有資料的確切大小和生命週期,因為它必須生成 相應的程式碼,以便上下移動堆疊指標。這一約束限制了程式的靈活性,所以雖然某些JAVA資料儲存在堆疊中——特別是物件引用,但是JAVA物件不儲存其中。


  3. 堆(heap)。

一種通用性的記憶體池(也存在於RAM中),用於存放所以的JAVA物件。堆不同於堆疊的好處是:編譯器不需要知道要從堆裡分配多少儲存區 域,也不必知道儲存的資料在堆裡存活多長時間。因此,在堆裡分配儲存有很大的靈活性。當你需要建立一個物件的時候,只需要new寫一行簡單的程式碼,當執行 這行程式碼時,會自動在堆裡進行儲存分配。當然,為這種靈活性必須要付出相應的程式碼。用堆進行儲存分配比用堆疊進行儲存儲存需要更多的時間。


  4. 靜態儲存(static storage)。

這裡的“靜態”是指“在固定的位置”。靜態儲存裡存放程式執行時一直存在的資料。你可用關鍵字static來標識一個物件的特定元素是靜態的,但JAVA物件本身從來不會存放在靜態儲存空間裡。


  5. 常量儲存(constant storage)。

常量值通常直接存放在程式程式碼內部,這樣做是安全的,因為它們永遠不會被改變。有時,在嵌入式系統中,常量本身會和其他部分分割離開,所以在這種情況下,可以選擇將其放在ROM中 .


  6. 非RAM儲存。

如果資料完全存活於程式之外,那麼它可以不受程式的任何控制,在程式沒有執行時也可以存在。 
   就速度來說,有如下關係: 
   暫存器 < 堆疊 < 堆 < 其他

      執行類過程:方法區找到方法--堆中例項化物件--呼叫棧(指向堆中例項)

第二部分

轉載自http://www.cnblogs.com/wangguoning/p/6109377.html

方法區
在一個jvm例項的內部,型別資訊被儲存在一個稱為方法區的記憶體邏輯區中。型別資訊是由類載入器在類載入時從類檔案中提取出來的。類(靜態)變數也儲存在方法區中。
 

jvm實現的設計者決定了型別資訊的內部表現形式。如,多位元組變數在類檔案是以big-endian儲存的,但在載入到方法區後,其存放形式由jvm根據不同的平臺來具體定義。
 

jvm在執行應用時要大量使用儲存在方法區中的型別資訊。在型別資訊的表示上,設計者除了要儘可能提高應用的執行效率外,還要考慮空間問題。根據不同的需求,jvm的實現者可以在時間和空間上追求一種平衡。
 

因為方法區是被所有執行緒共享的,所以必須考慮資料的執行緒安全。假如兩個執行緒都在試圖找lava的類,在lava類還沒有被載入的情況下,只應該有一個執行緒去載入,而另一個執行緒等待。
 

方法區的大小不必是固定的,jvm可以根據應用的需要動態調整。同樣方法區也不必是連續的。方法區可以在堆(甚至是虛擬機器自己的堆)中分配。jvm可以允許使用者和程式指定方法區的初始大小,最小和最大尺寸。
 

方法區同樣存在垃圾收集,因為通過使用者定義的類載入器可以動態擴充套件
Java程式,一些類也會成為垃圾。jvm可以回收一個未被引用類所佔的空間,以使方法區的空間最小。 

型別資訊
對每個載入的型別,jvm必須在方法區中儲存以下型別資訊: 
一 這個型別的完整有效名 
二 這個型別直接父類的完整有效名(除非這個型別是interface或是java.lang.Object,兩種情況下都沒有父類) 
三 這個型別的修飾符(public,abstract, final的某個子集) 
四 這個型別直接介面的一個有序列表 

型別名稱在java類檔案和jvm中都以完整有效名出現。在java原始碼中,完整有效名由類的所屬包名稱加一個".",再加上類名組成。例如,類Object的所屬包為java.lang,那它的完整名稱為java.lang.Object,但在類檔案裡,所有的"."都被斜槓“/”代替,就成為java/lang/Object。完整有效名在方法區中的表示根據不同的實現而不同。 

除了以上的基本資訊外,jvm還要為每個型別儲存以下資訊: 
型別的常量池( constant pool) 
域(Field)資訊 
方法(Method)資訊 
除了常量外的所有靜態(static)變數 

常量池
jvm為每個已載入的型別都維護一個常量池。常量池就是這個型別用到的常量的一個有序集合,包括實際的常量(string, 
integer, 和floating point常量)和對型別,域和方法的符號引用。池中的資料項象陣列項一樣,是通過索引訪問的。 
因為常量池儲存了一個型別所使用到的所有型別,域和方法的符號引用,所以它在java程式的動態連結中起了核心的作用。 

域資訊
jvm必須在方法區中儲存型別的所有域的相關資訊以及域的宣告順序, 
域的相關資訊包括: 
域名 
域型別 
域修飾符(public, private, protected,static,final   volatile, transient的某個子集) 
        
方法資訊
jvm必須儲存所有方法的以下資訊,同樣域資訊一樣包括宣告順序 
方法名 
方法的返回型別(或 void) 
方法引數的數量和型別(有序的) 
方法的修飾符(public, private, protected, static, final, synchronized, native, abstract的一個子集)除了abstract和native方法外,其他方法還有儲存方法的位元組碼(bytecodes)運算元棧和方法棧幀的區域性變數區的大小           
異常表 

類變數

(Class Variables 譯者:就是類的靜態變數,它只與類相關,所以稱為類變數) 
類變數被類的所有例項共享,即使沒有類例項時你也可以訪問它。這些變數只與類相關,所以在方法區中,它們成為類資料在邏輯上的一部分。在jvm使用一個類之前,它必須在方法區中為每個non-final類變數分配空間。 

常量(被宣告為final的類變數)的處理方法則不同,每個常量都會在常量池中有一個拷貝。non-final類變數被儲存在宣告它的 
類資訊內,而final類被儲存在所有使用它的類資訊內。 

對類載入器的引用
jvm必須知道一個型別是由啟動載入器載入的還是由使用者類載入器載入的。如果一個型別是由使用者類載入器載入的,那麼jvm會將這個類載入器的一個引用作為型別資訊的一部分儲存在方法區中。 

jvm在動態連結的時候需要這個資訊。當解析一個型別到另一個型別的引用的時候,jvm需要保證這兩個型別的類載入器是相同的。這對jvm區分名字空間的方式是至關重要的。 

對Class類的引用

jvm為每個載入的型別(譯者:包括類和介面)都建立一個java.lang.Class的例項。而jvm必須以某種方式把Class的這個例項和儲存在方法區中的型別資料聯絡起來。 

你可以通過Class類的一個靜態方法得到這個例項的引用// A method declared in class java.lang.Class: 
public static Class forName(String className); 

假如你呼叫forName("java.lang.Object"),你會得到與java.lang.Object對應的類物件。你甚至可以通過這個函式 
得到任何包中的任何已載入的類引用,只要這個類能夠被載入到當前的名字空間。如果jvm不能把類載入到當前名字空間, 
forName就會丟擲ClassNotFoundException。 
(譯者:熟悉COM的朋友一定會想到,在COM中也有一個稱為      類物件(Class Object)的東東,這個類物件主要      是實現一種工廠模式,而java由於有了jvm這個中間      層,類物件可以很方便的提供更多的資訊。這兩種類物件      都是Singleton的) 

也可以通過任一物件的getClass()函式得到類物件的引用,getClass被宣告在Object類中: 
// A method declared in class java.lang.Object: 
public final Class getClass(); 
例如,假如你有一個java.lang.Integer的物件引用,可以啟用getClass()得到對應的類引用。 

通過類物件的引用,你可以在執行中獲得相應類儲存在方法區中的型別資訊,下面是一些Class類提供的方法: 
// Some of the methods declared in class java.lang.Class: 
public String getName(); 
public Class getSuperClass(); 
public boolean isInterface(); 
public Class[] getInterfaces(); 
public ClassLoader getClassLoader(); 

這些方法僅能返回已載入類的資訊。getName()返回類的完整名,getSuperClass()返回父類的類物件,isInterface()判斷是否是介面。getInterfaces()返回一組類物件,每個類物件對應一個直接父介面。如果沒有,則返回一個長度為零的陣列。 
getClassLoader()返回類載入器的引用,如果是由啟動類載入器載入的則返回null。所有的這些資訊都直接從方法區中獲得。 

方法表
為了提高訪問效率,必須仔細的設計儲存在方法區中的資料資訊結構。除了以上討論的結構,jvm的實現者還可以新增一些其他的資料結構,如方法表。jvm對每個載入的非虛擬類的型別資訊中都添加了一個方法表,方法表是一組對類例項方法的直接引用(包括從父類繼承的方法)。jvm可以通過方法錶快速啟用例項方法。(譯者:這裡的方法表與C++中的虛擬函式表一樣,但java方法全都 是virtual的,自然也不用虛擬二字了。正像java宣稱沒有      指標了,其實java裡全是指標。更安全只是加了更完備的檢查機制,但這都是以犧牲效率為代價的,個人認為java的設計者      始終是把安全放在效率之上的,所有java才更適合於網路開發) 

一個例子 
為了顯示jvm如何使用方法區中的資訊,我們據一個例子,我們 
看下面這個類: 
class Lava { 
    private int speed = 5; // 5 kilometers per hour 
    void flow() { 
    } 
} 

class Volcano { 
    public static void main(String[] args) { 
        Lava lava = new Lava(); 
        lava.flow(); 
    } 
} 
下面我們描述一下main()方法的第一條指令的位元組碼是如何被執行的。不同的jvm實現的差別很大,這裡只是其中之一。 

為了執行這個程式,你以某種方式把“Volcano"傳給了jvm。有了這個名字,jvm找到了這個類檔案(Volcano.class)並讀入,它從類檔案提取了型別資訊並放在了方法區中,通過解析存在方法區中的位元組碼,jvm激活了main()方法,在執行時,jvm保持了一個指向當前類(Volcano)常量池的指標。 

注意jvm在還沒有載入Lava類的時候就已經開始執行了。正像大多數的jvm一樣,不會等所有類都載入了以後才開始執行,它只會在需要的時候才載入。 

main()的第一條指令告知jvm為列在常量池第一項的類分配足夠的記憶體。jvm使用指向Volcano常量池的指標找到第一項,發現是一個對Lava類的符號引用,然後它就檢查方法區看lava是否已經被載入了。 

這個符號引用僅僅是類lava的完整有效名”lava“。這裡我們看到為了jvm能儘快從一個名稱找到一個類,一個良好的資料結構是多麼重要。這裡jvm的實現者可以採用各種方法,如hash表,查詢樹等等。同樣的演算法可以用於Class類的forName()的實現。 

當jvm發現還沒有載入過一個稱為"Lava"的類,它就開始查詢並載入類檔案"Lava.class"。它從類檔案中抽取型別資訊並放在了方法區中。 

jvm於是以一個直接指向方法區lava類的指標替換了常量池第一項的符號引用。以後就可以用這個指標快速的找到lava類了。而這個替換過程稱為常量池解析(constant pool resolution)。在這裡我們替換的是一個native指標。 

jvm終於開始為新的lava物件分配空間了。這次,jvm仍然需要方法區中的資訊。它使用指向lava資料的指標(剛才指向volcano常量池第一項的指標)找到一個lava物件究竟需要多少空間。 

jvm總能夠從儲存在方法區中的型別資訊知道某型別物件需要的空間。但一個物件在不同的jvm中可能需要不同的空間,而且它的空間分佈也是不同的。(譯者:這與在C++中,不同的編譯器也有不同的物件模型是一個道理) 

一旦jvm知道了一個Lava物件所要的空間,它就在堆上分配這個空間並把這個例項的變數speed初始化為預設值0。假如lava的父物件也有例項變數,則也會初始化。 

當把新生成的lava物件的引用壓到棧中,第一條指令也結束了。下面的指令利用這個引用啟用java程式碼把speed變數設為初始值,5。另外一條指令會用這個引用啟用Lava物件的flow()方法。