Hotpot Java虛擬機Class對象是在方法區還是堆中

Class對象是存放在堆區的,不是方法區,這點很多人容易犯錯。類的元數據(元數據並不是類的Class對象。Class對象是加載的最終產品,類的方法代碼,變量名,方法名,訪問權限,返回值等等都是在方法區的)纔是存在方法區的。

方法區

在一個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()方法。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章