之前我們已經了解過“運行時資料區”的程式計數器、虛擬機堆疊、本地方法堆疊和堆空間,今天我們就來了解一下最后一個模塊——方法區,

簡介
創建物件時記憶體分配簡圖

《Java虛擬機規范》中明確說明:“盡管所有的方法區在邏輯上屬于堆的一部分,但一些簡單的實作可能不會選擇去進行垃圾收集或者進行壓縮,”
雖然 Java 虛擬機規范把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做 Non-Heap(非堆),目的應該是與 Java 堆區分開來,所以,方法區可以看作是一塊獨立于 Java 堆的記憶體空間,
方法區與 Java 堆一樣,是各個執行緒共享的記憶體區域,方法區在 JVM 啟動時就會被創建,并且它的實際的物理記憶體空間是可以不連續的,關閉 JVM 就會釋放這個區域的記憶體,
永久代、元空間
《java虛擬機規范》對如何實作方法區,不做統一要求,例如:BEA JRockit/IBM J9 中不存在永久代的概念,而對于 HotSpot 來說,在 jdk7 及以前,習慣上把方法區的實作稱為永久代,而從 jdk8 開始,使用元空間取代了永久代,
方法區是 Java 虛擬機規范中的概念,而永久代和元空間是 HotSpot 虛擬機對方法區的一種實作,通俗點講:如果把方法區比作介面的話,那永久代和元空間可以比作實作該介面的實作類,
直接記憶體
永久代、元空間并不只是名字變了,內部結構也進行了調整,永久代使用的是 JVM 的記憶體,而元空間使用的是本地的直接記憶體,
直接記憶體并不是 JVM 運行時資料區的一部分,因此不會受到 Java 堆的限制,但是它會受到本機總記憶體大小以及處理器尋址空間的限制,所以如果這部分記憶體也被頻繁的使用,依然會導致 OOM 錯誤的出現,
方法區的大小
方法區的大小是可以進行設定的,可以選擇固定大小也可以進行擴展,
jdk7 及以前
-XX:PermSize=N //方法區 (永久代) 初始分配空間,默認值為 20.75M
-XX:MaxPermSize=N //方法區 (永久代) 最大可分配空間,32位機器默認是64M,64位機器默認是82M
jdk8及以后
默認值依賴于平臺,windows下:
-XX:MetaspaceSize=N //方法區 (元空間) 初始分配空間,如果未指定此標志,則元空間將根據運行時的應用程式需求動態地重新調整大小,
-XX:MaxMetaspaceSize=N //方法區 (元空間) 最大可分配空間,默認值為 -1,即沒有限制
與永久代很大的不同就是,如果不指定大小的話,隨著更多類的創建,虛擬機會耗盡所有可用的系統記憶體,
方法區的大小決定了系統可以保存多少個類,如果系統定義了太多的類,比如:加載大量的第三方 jar 包、Tomcat 部署的工程過多、大量動態生成反射類等都會導致方法區溢位,拋出記憶體溢位錯誤,
永久代:OutOfMemoryError:PermGen space 元空間:OutOfMemoryError:Metaspace
至于如何解決 OOM 例外,將在以后的文章中講解!
jvisualvm
我們可以通過 JDK 自帶的 jvisualvm 工具來查看程式加載的類檔案:
例
public class MethodAreaDemo1 {
public static void main(String[] args) {
System.out.println("start...");
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("end...");
}
}
運行程式,可以看到一個簡單的程式就需要加載這么多的類檔案,

高水位線
對于一個64位的服務器端 JVM 來說,XX:MetaspaceSize=21 就是初始的高水位線,一旦觸及這個水位線,Full GC 將會被觸發并卸載沒用的類(即這些類對應的類加載器不再存活),然后這個高水位線將會重置,
新的高水位線的值取決于 GC 后釋放了多少元空間:
如果釋放的空間不足,那么在不超過 MaxMetaspaceSize 時,適當提高該值; 如果釋放空間過多,則適當降低該值,
如果初始化的高水位線設定過低,高水位線調整情況會發生很多次,通過垃圾回收器的日志可以觀察到 Full GC 多次呼叫,為了避免頻繁地GC,建議將
-XX :MetaspaceSize設定為一個相對較高的值,
內部結構
《深入理解Java虛擬機》書中對方法區存盤內容描述如下:它用于存盤已被虛擬機加載的型別資訊、常量、靜態變數、即時編譯器編譯后的代碼快取等,接下來我們就一起來看一下它的內部結構,

型別資訊
對每個加載的型別( 類 class、介面 interface、列舉 enum、注解 annotation),JVM 必須在方法區中存盤以下型別資訊:
這個型別的完整有效名稱(全名=包名.類名) 這個型別直接父類的完整有效名(對于 interface 或是 java. lang.Object ,都沒有父類) 這個型別的修飾符( public , abstract, final 的某個子集) 這個型別直接介面的一個有序串列
域(Field)資訊
JVM必須在方法區中保存型別的所有域(field,也稱為屬性)的相關資訊以及域的宣告順序; 域的相關資訊包括:域名稱、 域型別、域修飾符(public, private,protected, static, final, volatile, transient 的某個子集)
方法(Method)資訊
JVM 必須保存所有方法的以下資訊,同域資訊一樣包括宣告順序:
方法名稱 方法的回傳型別(或void) 方法引數的數量和型別(按順序) 方法的修飾符(public, private, protected, static, final,synchronized, native , abstract 的一個子集) 方法的位元組碼(bytecodes)、運算元堆疊、區域變數表及大小( abstract 和 native 方法除外) 例外表( abstract 和 native 方法除外)每個例外處理的開始位置、結束位置、代碼處理在程式計數器中的偏移地址、被捕獲的例外類的常量池索引
non-final 的類變數
靜態變數和類關聯在一起,隨著類的加載而加載,他們成為類資料在邏輯上的一部分 類變數被類的所有實體所共享,即使沒有類實體你也可以訪問它,
我們可以通過例子來查看:
public class MethodAreaDemo2 {
public static void main(String[] args) {
Order order = null;
order.hello();
System.out.println(order.count);
}
}
class Order {
public static int count = 1;
public static final int number = 2;
public static void hello() {
System.out.println("hello!");
}
}
運行結果為:
hello!
1
可以打開 IDEA 的 Terminal 視窗,在 MethodAreaDemo2.class 所在的路徑下,輸入 javap -v -p MethodAreaDemo2.class 命令

通過圖片我們可以看出被宣告為 final 的類變數的處理方法是不一樣的,全域常量在編譯的時候就被分配了,
運行時常量池
說到運行時常量池,我們先來了解一下什么是常量池表,
常量池表
一個有效的位元組碼檔案中除了包含類的版本資訊、欄位、方法以及介面等描述資訊外,還包含一項資訊那就是常量池表(Constant Pool Table),里邊存盤著數量值、字串值、類參考、欄位參考和方法參考,

為什么位元組碼檔案需要常量池?
java 源檔案中的類、介面,編譯后會產生一個位元組碼檔案,而位元組碼檔案需要資料支持,通常這種資料會很大,以至于不能直接存放到位元組碼中,換一種方式,可以將指向這些資料的符號參考存到位元組碼檔案的常量池中,這樣位元組碼只需使用常量池就可以在運行時通過動態鏈接找到相應的資料并使用,
運行時常量池
運行時常量池( Runtime Constant Pool)是方法區的一部分,類加載器加載位元組碼檔案時,將常量池表加載進方法區的運行時常量池,運行時常量池中包含多種不同的常量,包括編譯期就已經明確的數值字面量,也包括到運行期決議后才能夠獲得的方法或者欄位參考,此時不再是常量池中的符號地址了,這里換為真實地址,
運行時常量池,相對于 Class 檔案常量池的另一重要特征是:具備動態性,比如
String.intern(),
演進細節
針對的是 Hotspot 的虛擬機:
jdk1.6 及之前:有永久代 ,靜態變數存放在永久代上; jdk1.7:有永久代,但已經逐步“去永久代”,字串常量池、靜態變數移除,保存在堆中; jdk1.8及之后: 無永久代,型別資訊、欄位、方法、常量保存在本地記憶體的元空間,但字串常量池、靜態變數仍在堆中;
演變示例圖

為什么要將永久代替換為元空間呢?
永久代使用的是 JVM 的記憶體,受 JVM 設定的記憶體大小限制;元空間使用的是本地直接記憶體,它的最大可分配空間是系統可用記憶體的空間,因為元空間里存放的是類的元資料,所以隨著記憶體空間的增大,能加載的類就更多了,相應的溢位的機率會大大減小, 在 JDK8,合并 HotSpot 和 JRockit 的代碼時,JRockit 從來沒有一個叫永久代的東西,合并之后就沒有必要額外的設定這么一個永久代的地方了, 對永久代進行調優是很困難的,
StringTable 為什么要調整
因為永久代的回收效率很低,在 full gc 的時候才會觸發,而 full GC 是老年代的空間不足、永久代不足時才會觸發,這就導致了StringTable 回收效率不高,而我們開發中會有大量的字串被創建,回收效率低,導致永久代記憶體不足,放到堆里,能及時回收記憶體,
垃圾回收
相對而言,垃圾收集行為在這個區域是比較少出現的,但并非資料進入方法區后就“永久存在”了,方法區的垃圾收集主要回收兩部分內容:常量池中廢奔的常量和不再使用的型別,
方法區內常量池中主要存放字面量和符號參考兩大類常量:
字面量比較接近 Java 語言層次的常量概念,如文本字串、被宣告為 final 的常量值等, 符號參考則屬于編譯原理方面的概念,包括類和介面的全限定名、欄位的名稱和描述符、方法的名稱和描述符,
HotSpot 虛擬機對常量池的回收策略是很明確的,只要常量池中的常量沒有被任何地方參考,就可以被回收,
型別判定
判定一個常量是否“廢棄”還是相對簡單,而要判定一個型別是否屬于“不再被使用的類”的條件就比較苛刻了,需要同時滿足下面三個條件:
該類所有的實體都已經被回收,也就是 Java 堆中不存在該類及其任何派生子類的實體; 加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載器的場景,如OSGi、JSP的重加載等,否則通常是很難達成的; 該類對應的 java.lang.Class 物件沒有在任何地方被參考,無法在任何地方通過反射訪問該類的方法,
Java 虛擬機被允許對滿足上述三個條件的無用類進行回收,這里說的僅僅是“被允許”,而并不是和物件一樣,沒有參考了就必然會回收,
以上就是今天的全部內容了,如果你有不同的意見或者更好的idea,歡迎聯系阿Q,添加阿Q可以加入技術交流群參與討論呦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/315864.html
標籤:Java
