標籤:包名 ack ioc 如何使用 處理 執行 nat 需求 函數
一、方法區簡介
方法區,Method Area, 對於習慣在HotSpot虛擬機器上開發和部署程式的開發人員來說,很多人願意把方法區稱為“永久代”(Permanent Generation),本質上兩者並不等價,僅僅是因為HotSpot虛擬機器的設計團隊選擇把GC分代收集擴充至方法區,或者說使用永久代來實現方法區而已。對於其他虛擬機器(如BEA JRockit、IBM J9等)來說是不存在永久代的概念的。
主要存放已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的代碼等資料(比如spring使用IOC或者AOP建立bean時,或者使用cglib,反射的形式動態產生class資訊等)。
注意:JDK 6 時,String等字串常量的資訊是置於方法區中的,但是到了JDK 7 時,已經移動到了Java堆。所以,方法區也好,Java堆也罷,到底詳細的儲存了什麼,其實沒有具體定論,要結合不同的JVM版本來分析。
二、方法區的幾個特點
- 方法區在一個jvm執行個體的內部,類型資訊被儲存在一個稱為方法區的記憶體邏輯區中。類型資訊是由類載入器在類載入時從類檔案中提取出來的。類(靜態)變數也儲存在方法區中。
- jvm實現的設計者決定了類型資訊的內部表現形式。如,多位元組變數在類檔案是以big-endian儲存的,但在載入到方法區後,其存放形式由jvm根據不同的平台來具體定義。
- jvm在運行應用時要大量使用儲存在方法區中的類型資訊。在類型資訊的表示上,設計者除了要儘可能提高應用的運行效率外,還要考慮空間問題。根據不同的需求,jvm的實現者可以在時間和空間上追求一種平衡。
- 因為方法區是被所有線程共用的,所以必須考慮資料的安全執行緒。假如兩個線程都在試圖找lava的類,在lava類還沒有被載入的情況下,只應該有一個線程去載入,而另一個線程等待。
- 方法區的大小不必是固定的,jvm可以根據應用的需要動態調整。同樣方法區也不必是連續的。方法區可以在堆(甚至是虛擬機器自己的堆)中分配。jvm可以允許使用者和程式指定方法區的初始大小,最小和最大尺寸。
- 方法區同樣存在垃圾收集,因為通過使用者定義的類載入器可以動態擴充java程式,一些類也會成為垃圾。jvm可以回收一個未被引用類所佔的空間,以使方法區的空間最小。
三、方法區的結構資訊1) 類型資訊
- 對每個載入的類型,jvm必須在方法區中儲存以下類型資訊:
- 這個類型的完整有效名
- 這個類型直接父類的完整有效名(除非這個類型是interface或是java.lang.Object,兩種情況下都沒有父類)
- 這個類型的修飾符(public,abstract, final的某個子集)
- 這個類型直接介面的一個有序列表
類型名稱在java類檔案和jvm中都以完整有效名出現。在java原始碼中,完整有效名由類的所屬包名稱加一個".",再加上類名組成。例如,類Object的所屬包java.lang,那它的完整名稱為java.lang.Object,但在類檔案裡,所有的"."都被斜杠“/”代替,就成為java/lang/Object。完整有效名在方法區中的表示根據不同的實現而不同。
2) 常量池
jvm為每個已載入的類型都維護一個常量池。常量池就是這個類型用到的常量的一個有序集合,包括實際的常量(string,integer, 和floating point常量)和對類型,域和方法的符號引用。池中的資料項目像數組項一樣,是通過索引訪問的。
3)域資訊
jvm必須在方法區中檔案類型的所有域的相關資訊以及域的聲明順序,域的相關資訊包括:
- 網域名稱
- 域類型
- 域修飾符(public, private, protected,static,final,volatile, transient的某個子集)
4)方法資訊
jvm必須儲存所有方法的以下資訊,同樣域資訊一樣包括聲明順序
- 方法名
- 方法的傳回型別(或 void)
- 方法參數的數量和類型(有序的)
- 方法的修飾符(public, private, protected, static, final, synchronized, native, abstract的一個子集)除了abstract和native方法外,其他方法還有儲存方法的位元組碼(bytecodes)運算元棧和方法棧幀(堆棧以幀為單位儲存線程的狀態)的局部變數區的大小
- 異常表
5)類變數與類常量
類變數(Class Variables:就是類的靜態變數,它只與類相關,所以稱為類變數)
類變數被類的所有執行個體共用,即使沒有類執行個體時你也可以訪問它。這些變數只與類相關,所以在方法區中,它們成為類資料在邏輯上的一部分。在jvm使用一個類之前,它必須在方法區中為每個non-final類變數分配空間。
常量(被聲明為final的類變數)的處理方法則不同,每個常量都會在常量池中有一個拷貝。non-final類變數被儲存在聲明它的類資訊內,而final類被儲存在所有使用它的類資訊內。
6)對類載入器的引用
jvm必須知道一個類型是由啟動載入器載入的還是由使用者類載入器載入的。如果一個類型是由使用者類載入器載入的,那麼jvm會將這個類載入器的一個引用作為類型資訊的一部分儲存在方法區中。jvm在動態連結的時候需要這個資訊。當解析一個類型到另一個類型的引用的時候,jvm需要保證這兩個類型的類載入器是相同的。這對jvm區分名字空間的方式是至關重要的。
7)對Class類的引用
jvm為每個載入的類型(譯者:包括類和介面)都建立一個java.lang.Class的執行個體。而jvm必須以某種方式把Class的這個執行個體和儲存在方法區中的類型資料聯絡起來。
你可以通過Class類的一個靜態方法得到這個執行個體的引用:public static Class forName(String className);
假如你調用forName("java.lang.Object"),你會得到與java.lang.Object對應的類對象。你甚至可以通過這個函數得到任何包中的任何已載入的類引用,只要這個類能夠被載入到當前的名字空間。如果jvm不能把類載入到當前名字空間,forName就會拋出ClassNotFoundException。
也可以通過任一對象的getClass()函數得到類對象的引用,getClass被聲明在Object類中:public final Class getClass();
例如,假如你有一個java.lang.Integer的對象引用,可以啟用getClass()得到對應的類引用。
通過類對象的引用,你可以在運行中獲得相應類儲存在方法區中的類型資訊,下面是一些Class類提供的方法:
- public String getName();
- public Class getSuperClass();
- public boolean isInterface();
- public Class[] getInterfaces();
- public ClassLoader getClassLoader();
這些方法僅能返回已載入類的資訊。getName()返回類的完整名,getSuperClass()返回父類的類對象,isInterface ()判斷是否是介面。getInterfaces()返回一組類對象,每個類對象對應一個直接父介面。如果沒有,則返回一個長度為零的數組。
getClassLoader()返回類載入器的引用,如果是由啟動類載入器載入的則返回null。所有的這些資訊都直接從方法區中獲得。
8)方法表
為了提高訪問效率,必須仔細的設計儲存在方法區中的資料資訊結構。除了以上討論的結構,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變數設為初始值,另外一條指令會用這個引用啟用Lava對象的flow()方法。
解釋:符號引用與直接引用
1、符號引用(Symbolic References):
符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能夠無歧義的定位到目標即可。例如,在Class檔案中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等類型的常量出現。符號引用與虛擬機器的記憶體布局無關,引用的目標並不一定載入到記憶體中。在Java中,一個java類將會編譯成一個class檔案。在編譯時間,java類並不知道所引用的類的實際地址,因此只能使用符號引用來代替。比如org.simple.People類引用了org.simple.Language類,在編譯時間People類並不知道Language類的實際記憶體位址,因此只能使用符號org.simple.Language(假設是這個,當然實際中是由類似於CONSTANT_Class_info的常量來表示的)來表示Language類的地址。各種虛擬機器實現的記憶體布局可能有所不同,但是它們能接受的符號引用都是一致的,因為符號引用的字面量形式明確定義在Java虛擬機器規範的Class檔案格式中。
2、直接引用:(1)直接指向目標的指標(比如,指向“類型”【Class對象】、類變數、類方法的直接引用可能是指向方法區的指標)(2)相對位移量(比如,指向執行個體變數、執行個體方法的直接引用都是位移量)(3)一個能間接定位到目標的控制代碼直接引用是和虛擬機器的布局相關的,同一個符號引用在不同的虛擬機器執行個體上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經被載入入記憶體中了。
java虛擬機器:方法區