java方法區詳解
方法區
保存在着被加載過的每一個類的信息;這些信息由類加載器在加載類的時候,從類的源文件中抽取出來;static變量信息也保存在方法區中;
可以看做是將類(Class)的元數據,保存在方法區里;
方法區是線程共享的;當有多個線程都用到一個類的時候,而這個類還未被加載,則應該只有一個線程去加載類,讓其他線程等待;
方法區的大小不必是固定的,jvm可以根據應用的需要動態調整。jvm也可以允許用戶和程序指定方法區的初始大小,最小和最大限制;
方法區同樣存在垃圾收集,因為通過用戶定義的類加載器可以動態擴展Java程序,這樣可能會導致一些類,不再被使用,變為垃圾。這時候需要進行垃圾清理。
圖例(方法區中都保存什么)

類型信息
包括以下幾點:
類的完整名稱(比如,java.long.String)
類的直接父類的完整名稱
類的直接實現接口的有序列表(因為一個類直接實現的接口可能不止一個,因此放到一個有序表中)
類的修飾符
可以看做是,對一個類進行登記,這個類的名字叫啥,他粑粑是誰、有沒有實現接口, 權限是啥;
類型的常量池 (即運行時常量池)
每一個Class文件中,都維護着一個常量池(這個保存在類文件里面,不要與方法區的運行時常量池搞混),里面存放着編譯時期生成的各種字面值和符號引用;這個常量池的內容,在類加載的時候,被復制到方法區的運行時常量池 ;
字面值:就是像string, 基本數據類型,以及它們的包裝類的值,以及final修飾的變量,簡單說就是在編譯期間,就可以確定下來的值;
符號引用:不同於我們常說的引用,它們是對類型,域和方法的引用,類似於面向過程語言使用的前期綁定,對方法調用產生的引用;
存在這里面的數據,類似於保存在數組中,外部根據索引來獲得它們 ;
字段信息
- 聲明的順序
- 修飾符
- 類型
- 名字
方法信息
- 聲明的順序
- 修飾符
- 返回值類型
- 名字
- 參數列表(有序保存)
- 異常表(方法拋出的異常)
- 方法字節碼(native、abstract方法除外,)
- 操作數棧和局部變量表大小
類變量(即static變量)
非final類變量
在java虛擬機使用一個類之前,它必須在方法區中為每個非final類變量分配空間。非final類變量存儲在定義它的類中;
final類變量(不存儲在這里)
由於final的不可改變性,因此,final類變量的值在編譯期間,就被確定了,因此被保存在類的常量池里面,然后在加載類的時候,復制進方法區的運行時常量池里面 ;final類變量存儲在運行時常量池里面,每一個使用它的類保存着一個對其的引用;
對類加載器的引用
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()方法。
=========================================================================================================================
轉載自:https://blog.csdn.net/youngyouth/article/details/79933612