學習JVM必看的書籍無疑是《深入理解Java虛擬機器》這本書了,在書中,關於執行時資料區域模型是這樣描述的:
在這裡我們只針對HotSpot VM來說,它是OracleJDK和OpenJDK中所帶的虛擬機器,也是目前使用範圍最廣的Java虛擬機器。在JDK7之前,這樣的模型是正確的。但是到了JDK8,如圖示紅的部分,做了一些優化。
什麼是方法區,什麼是永久代,執行時常量池又是什麼
- “方法區”(Method Area),是執行緒共享的區域,用於儲存已被虛擬機器載入的類資訊,常量,靜態變數等資料。首先我們要知道,方法區是JVM的一種規範,是一個概念,而這個方法區的具體實現由各個虛擬機器廠商去實現。
- “永久代”(Permanent Generation)就是HotSpot虛擬機器對於方法區的實現,也僅僅是針HotSpot才有的。
- “執行時常量池”是方法區的一部分。用於存放編譯期生成的各種字面量和符號引用。其特性是具備動態性。
優化一:字串常量池從永久代劃到Java堆
由於常量池具備動態性,在程式執行過程中會有大量的字串常量在執行時常量池裡產生,此時如果放在永久代,則無法恰當的設定永久代的大小,容易出現效能問題和記憶體溢位。下面一個例子證明在JDK8中,字串常量池已經放在堆中:
String.intern()方法的作用是返回一個字串引用,引用的是字串常量池中的字串(字面量),我們先來驗證一下這個方法:
public class StringConstantsPoolTest {
public static void main(String[] args) {
String str = "abc"; // str儲存在常量池
String str2 = new String("abc"); // str2 儲存在堆中
System.out.println(str == str2); // 結果為false ,堆中的引用並不等於常量池中的引用
str2 = str2.intern(); // 獲取str2在常量池中的引用
System.out.println(str == str2);
}
}
複製程式碼
結果如下:
證明 String.intern()方法返回了一個在常量池中的引用。 下面驗證字串常量池在堆中: 設定JVM引數:
-Xms10m -Xmx10m -XX:-UseGCOverheadLimit
public static void main(String[] args) {
List<String> list = new ArrayList();
int i = 0;
while(true){
list.add(String.valueOf(i++).intern());
}
}
複製程式碼
結果如下:
我們看到這時報的是Java堆空間記憶體溢位,說明字串常量池是在堆中,注意,此時僅僅是字串常量池轉移到了堆中,但是執行時常量池依舊還是在方法區裡
優化二:移除了永久代,引入“元空間”(Metaspace)
為什麼移除永久代?
- 方法區大小難以設定,容易發生記憶體溢位。永久代會存放Class的相關資訊,一般這些資訊在編譯期間就能確定大小。但是如果是在一些需要動態生成大量Class的應用中,如:Spring的動態代理、大量的JSP頁面或動態生成JSP頁面等,由於方法區的大小在一開始就要分配好,因此就能難確定大小,容易出現記憶體溢位
- GC複雜且效率低。方法區儲存了類的後設資料資訊和各種常量,它的記憶體回收目標理應當是對這些型別的解除安裝和常量的回收。但由於這些資料被類的例項引用,解除安裝條件變得複雜且嚴格,回收不當會導致堆中的類例項失去後設資料資訊和常量資訊。因此,回收方法區記憶體不是一件簡單高效的事情。
- 促進HotSpot JVM與JRockit VM的融合。JRockit沒有方法區,移除永久代可以促進HotSpot JVM與JRockit VM的融合。
什麼是元空間(Metaspace),為什麼引入元空間
元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機器中,而是使用本地記憶體。因此,預設情況下,元空間的大小僅受本地記憶體限制。
元空間的特點:
- 每個載入器有專門的儲存空間。
- 不會單獨回收某個類。
- 元空間裡的物件的位置是固定的。
- 如果發現某個載入器不再存活了,會把相關的空間整個回收。
總結
最終JVM(HotSpot)執行時資料區域模型如下: