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

http://www.cnblogs.com/paddix/p/5309550.htmlhtml

1、JVM 內存模型java

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

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

棧溢出測試源碼:jsp

1性能

2測試

3url

4spa

5.net

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

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();

        }

    }

}

代碼段 1

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

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

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

  二、本地方法棧:

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

  三、PC 寄存器:

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

  四、堆

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

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」的內存溢出:

1

2

3

4

package com.paddx.test.memory;

 

public class Test {

}

 代碼段 3

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

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 " 異常了。這裏之因此採用 JDK 1.7,是由於在 JDK 1.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。咱們能夠經過一段程序來比較 JDK 1.6 與 JDK 1.7及 JDK 1.8 的區別,以字符串常量爲例:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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的指數級不斷的生成新的字符串,這樣能夠比較快速的消耗內存。咱們經過 JDK 1.六、JDK 1.7 和 JDK 1.8 分別運行:

JDK 1.6 的運行結果:

JDK 1.7的運行結果:

JDK 1.8的運行結果:

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

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

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

  除了上面兩個指定大小的選項之外,還有兩個與 GC 相關的屬性:
  -XX:MinMetaspaceFreeRatio,在GC以後,最小的Metaspace剩餘空間容量的百分比,減小爲分配空間所致使的垃圾收集
  -XX:MaxMetaspaceFreeRatio,在GC以後,最大的Metaspace剩餘空間容量的百分比,減小爲釋放空間所致使的垃圾收集

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

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

4、總結

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

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

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

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

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

相關文章
相關標籤/搜索