(轉)Java8內存模型-永久代(PermGen)和元空間(Metaspace)

原文連接:https://www.cnblogs.com/paddix/p/5309550.htmlhtml

1、JVM內存模型

根據jvm規範,jvm內存共分爲虛擬機棧、堆、方法區、程序計算器、本地方法棧五個部分。java

一、虛擬機棧程序員

  每一個線程都有一個私有的棧,隨着線程的建立而建立。棧裏面粗糙你這的是一種叫作「棧幀」的東西,每一個方法會建立一個棧幀,棧幀中存放了局部變量(基本數據類型和對象引用)、操做數棧、方法出口等信息。棧的大小能夠固定也能夠動態擴展。當棧調用深度大於jvm所容許的範圍,會拋出StackOverflowError的錯誤,不過這個深度範圍不是一個恆定的值,下面的程序能夠測試一下這個結果:數組

棧溢出測試源碼:jvm

package com.paddx.test.memory; public class StackErrorMock { private static int index = 1; public void call(){ index++; call(); } public static void main(String[] args) { StackErrorMock mock = new StackErrorMock(); try { mock.call(); }catch (Throwable e){ System.out.println("Stack deep : "+index); e.printStackTrace(); } } }

代碼段1jsp

運行三次,能夠看出每次棧的深度都是不同的,輸出結果以下:性能

至於紅色框裏的值是怎麼出來的,就須要深刻到jvm的源碼中才能探討,這裏不做詳細闡述。測試

虛擬機棧除了上述錯誤外,還有另外一種錯誤,那就是當申請不到空間時,會拋出OutOfMemoryError。這理由一個小細節須要注意,catch捕獲的是Threowable,而不是Exception。由於StackOverflowError和OutOfMemoryError都不屬於Exception的子類url

二、本地方法棧spa

  這部分主要與虛擬機用到的native方法有關,通常狀況下,Java應用程序員並不須要關心這部分的內容

三、PC寄存器

  也叫作程序計數器。jvm支持多個線程同時運行,每一個線程都有本身的程序計數器。假若當前執行的是JVM的方法,則該寄存器中保存當前執行指令的地址;假若執行的是native方法,則pc寄存器中爲空

四、堆

  堆內存是jvm全部線程共享的部分,在虛擬機啓動的時候就已經建立。全部的對象和數組都在堆上進行分配。這部分空間可經過GC進行回收。當申請不到空間時會拋出OutOfMemoryError。下面咱們簡單的模擬一個堆內存溢出的狀況:

package com.paddx.test.memory; import java.util.ArrayList; import java.util.List; public class HeapOomMock { public static void main(String[] args) { List<byte[]> list = new ArrayList<byte[]>(); int i = 0; boolean flag = true; while (flag){ try { i++; list.add(new byte[1024 * 1024]);//每次增長一個1M大小的數組對象 }catch (Throwable e){ e.printStackTrace(); flag = false; System.out.println("count="+i);//記錄運行的次數  } } } }

代碼段2

運行上述代碼,輸出結果以下:

注意,這裏指定了堆內存的大小爲16M,因此這個地方顯示的count=14(這個數字不是固定的),至於爲何會是14或其餘數字,須要根據GC日誌來判斷,具體緣由會在下篇文章中給你們解釋

五、方法區

  方法區也是全部線程共享。主要用於存儲類的信息、常量池、方法數據、方法代碼等。方法區邏輯上屬於堆的一部分,可是爲了與堆進行區分,一般又叫「非堆」。關於方法區內存溢出的問題會在下文中詳細探討

2、PermGen(永久代)

絕大部分java程序員應該都見過「java.lang.OutOfMemoryError: PermGen space」這個異常。這裏的「PermGen space」其實指的就是方法區。不過方法區和「PermGen space」又有着本質的區別。前者是JVM的規範,然後者則是jvm規範的一種實現,而且只有HotSpot纔有「PermGen space」,而對於其餘類型的虛擬機,如JRockit(Oracle)、J9(IBM)並無「PermGen space」。因爲方法區主要存儲類的相關信息,因此對於動態生成類的狀況比較容易出現永久代的內存溢出。最典型的場景就是,在jsp頁面比較多的狀況,容易出現永久代內存溢出。咱們如今經過動態生成類來模擬「PermGen space」的內存溢出:

package com.paddx.test.memory; public class Test { }

代碼段3

 

package com.paddx.test.memory; import java.io.File; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.List; public class PermGenOomMock{ public static void main(String[] args) { URL url = null; List<ClassLoader> classLoaderList = new ArrayList<ClassLoader>(); try { url = new File("/tmp").toURI().toURL(); URL[] urls = {url}; while (true){ ClassLoader loader = new URLClassLoader(urls); classLoaderList.add(loader); loader.loadClass("com.paddx.test.memory.Test"); } } catch (Exception e) { e.printStackTrace(); } } }

代碼段4

運行結果以下: 

 

  本例中使用的JDk版本是1.7,指定的PermGen區的大小爲8M。經過每次生成不一樣URLClassLoader對象來加載Test類,從而生成不一樣的類對象,這樣就能看到咱們熟悉的「java.lang.OutOfMemoryError: PermGen space」異常了。這裏之因此此阿勇JDk1.7,是由於在JDK1.8中,HotSpot已經沒有「PermGen space」這個區間了,取而代之的是一個叫作Metaspace(元空間)的東西。下面咱們就來看看Metaspace與PermGen space的區別

3、Metaspace(元空間)

其實,溢出永久代的工做從JDK1.7就開始了。JDK1.7中,存儲在永久代的部分數據就已經轉移到了Java Heap或者Native Heap。但永久代仍存在於JDK1.7中,並無徹底移除,譬如符號引用(Symbols)轉移到了native heap;字面量(interned strings)轉移到了java heap;類的靜態變量(class statics)轉移到了java heap。咱們能夠經過一段程序來比較jdk1.6與jdk1.7及jdk1.8的區別,以字符串常量爲例:

package com.paddx.test.memory; import java.util.ArrayList; import java.util.List; public class StringOomMock { static String base = "string"; public static void main(String[] args) { List<String> list = new ArrayList<String>(); for (int i=0;i< Integer.MAX_VALUE;i++){ String str = base + base; base = str; list.add(str.intern()); } } }

這段程序以2的指數級不斷的生成新的字符串,這樣能夠比較快速的消耗內存。經過jdk1.六、jdk1.7和jdk1.8分別運行

 

jdk1.6的運行結果:

 

jdk1.7的運行結果: 

 

jdk1.8的運行結果: 

  從上述結果能夠看出,jdk1.6下,會出現「PermGen space」的內存溢出,而在JDK1.7和jdk1.8中,會出現堆內存溢出,而且jdk1.8中PermSize和MaxPermGen已經無效。所以,能夠大體驗證jdk1.7和1.8將字符串常量由永久代轉移到堆中,而且jdk1.8中已經不存在永久代的結論。如今咱們看看元空間究竟是一個什麼東西?

  元空間的本質和永久代相似,都是對jvm規範中方法區的實現。不過元空間與永久代最大的區別在於:元空間並不在虛擬機中,而是使用本地內存。所以默認狀況下,元空間的大小僅受本地內存的限制,但能夠經過如下參數來指定元空間的大小:

  -XX:MetaspaceSize,初始化空間大小,達到改值就會觸發垃圾收集進行類型卸載,同時GC會對改值進行調整:若是釋放了大量的空間,就適當下降該值;若是釋放了不多的空間,那麼在不超過MaxMetaspaceSize時,適當提升該值。

  -XX:MaxMetaspaceSize,最大空間,默認是沒有限制的。

   除了上面兩個指定大小的選項之外,還有兩個與GC相關的屬性:

  -XX:MinMetaspaceFreeRatio,在gc以後,最小的Metaspace剩餘空間容量的百分比,減小爲分配空間所致使的垃圾收集

  -XX:MinMetaspaceFreeRatio,在gc以後,最大的Metaspace剩餘空間容量的百分比,減小爲釋放空間所致使的垃圾收集

如今咱們在JDK8下從新運行一下代碼段4,不過此次再也不指定PermSize和MaxPermSize。而是指定MetaSpaceSize和MaxMetaSpaceSize的大小。輸出結果以下:

 

從輸出結果能夠看書,此次再也不出現永久代溢出,而是出現了元空間溢出。

4、總結

經過上面分析,你們應該大體瞭解了JVM的內存劃分,也清楚了JDK8中永久代向元空間的轉換。不過你們應該都有一個疑問,就是爲何要作這個轉換?因此,最後給你們總結如下幾點緣由:

一、字符串存在永久代中,容易出現性能問題和內存溢出

二、類及方法的信息等比較肯定其大小,所以對於永久代的大小指定比較困難,過小容易出現永久代溢出,太大容易致使老年代溢出

三、永久代會爲GC帶來沒必要要的複雜度,而且回收效率偏低

四、Oracle可能會將HotSpot與JRockit合二爲一

相關文章
相關標籤/搜索