方法區(關於java虛擬機器記憶體的那些事)
《深入理解 java 虛擬機器》 讀書擴充套件
作者:淮左白衣
寫於 2018年4月13日21:26:05
目錄
方法區
儲存在著被載入過的每一個類的資訊;這些資訊由類載入器在載入類的時候,從類的原始檔中抽取出來;static變數資訊也儲存在方法區中;
可以看做是將類(Class)的元資料,儲存在方法區裡;
方法區是執行緒共享的;當有多個執行緒都用到一個類的時候,而這個類還未被載入,則應該只有一個執行緒去載入類,讓其他執行緒等待;
方法區的大小不必是固定的,jvm可以根據應用的需要動態調整。jvm也可以允許使用者和程式指定方法區的初始大小,最小和最大限制;
方法區同樣存在垃圾收集,因為通過使用者定義的類載入器可以動態擴充套件Java程式,這樣可能會導致一些類,不再被使用,變為垃圾。這時候需要進行垃圾清理。
圖例(方法區中都儲存什麼)
圖片來源:圖片源地址
型別資訊
包括以下幾點:
- 類的完整名稱(比如,java.long.String)
- 類的直接父類的完整名稱
- 類的直接實現介面的有序列表(因為一個類直接實現的介面可能不止一個,因此放到一個有序表中)
- 類的修飾符
可以看做是,對一個類進行登記,這個類的名字叫啥,他粑粑是誰、有沒有實現介面, 許可權是啥;
型別的常量池 (即執行時常量池)
每一個Class檔案中,都維護著一個常量池
字面值:就是像string, 基本資料型別,以及它們的包裝類的值,以及final修飾的變數,簡單說就是在編譯期間,就可以確定下來的值;
符號引用:不同於我們常說的引用,它們是對型別,域和方法的引用,類似於面向過程語言使用的前期繫結,對方法呼叫產生的引用;
存在這裡面的資料,類似於儲存在陣列中,外部根據索引來獲得它們 ;
欄位資訊
- 宣告的順序
- 修飾符
- 型別
- 名字
方法資訊
- 宣告的順序
- 修飾符
- 返回值型別
- 名字
- 引數列表(有序儲存)
- 異常表(方法丟擲的異常)
- 方法位元組碼(native、abstract方法除外,)
- 運算元棧和區域性變量表大小
類變數(即static變數)
非final類變數
在java虛擬機器使用一個類之前,它必須在方法區中為每個非final類變數分配空間。非final類變數儲存在定義它的類中;
final類變數(不儲存在這裡)
由於final的不可改變性,因此,final類變數的值在編譯期間,就被確定了,因此被儲存在類的常量池裡面,然後在載入類的時候,複製進方法區的執行時常量池裡面 ;final類變數儲存在執行時常量池裡面,每一個使用它的類儲存著一個對其的引用;
對類載入器的引用
jvm必須知道一個型別是由啟動載入器載入的還是由使用者類載入器載入的。如果一個型別是由使用者類載入器載入的,那麼jvm會將這個類載入器的一個引用作為型別資訊的一部分儲存在方法區中。
jvm在動態連結的時候需要這個資訊。當解析一個型別到另一個型別的引用的時候,jvm需要保證這兩個型別的類載入器是相同的。這對jvm區分名字空間的方式是至關重要的。
對Class類的引用
jvm為每個載入的類都建立一個java.lang.Class的例項(儲存在堆上)。而jvm必須以某種方式把Class的這個例項和儲存在方法區中的型別資料(類的元資料)聯絡起來, 因此,類的元資料裡面儲存了一個Class物件的引用;
方法表
(以下是摘抄)
為了提高訪問效率,必須仔細的設計儲存在方法區中的資料資訊結構。除了以上討論的結構,jvm的實現者還可以新增一些其他的資料結構,如方法表。jvm對每個載入的非虛擬類的型別資訊中都添加了一個方法表,方法表是一組對類例項方法的直接引用(包括從父類繼承的方法。jvm可以通過方法錶快速啟用例項方法。(譯者:這裡的方法表與C++中的虛擬函式表一樣,但java方法全都 是virtual的,自然也不用虛擬二字了。正像java宣稱沒有 指標了,其實java裡全是指標。更安全只是加了更完備的檢查機制,但這都是以犧牲效率為代價的,個人認為java的設計者 始終是把安全放在效率之上的,所有java才更適合於網路開發)
(摘抄)JVM如何使用方法區裡面的資料的
一個例子
為了顯示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()方法。