1. 程式人生 > >深入理解JVM筆記之記憶體管理機制

深入理解JVM筆記之記憶體管理機制

執行時資料區域

程式計數器

每個執行緒都有一個程式計數器(PC),是當前執行緒所執行的位元組碼的行號指示器,通過改變程式計數器的值來選取下一條指令。各執行緒之間的計數器互不影響,是執行緒私有的記憶體。 
如果執行緒執行的是一個JAVA方法,則計數器記錄的為正在執行的位元組碼指令的地址,如果執行的是Natvie方法,這計數器的值為空(Undifined)。 
程式計算器所在的記憶體區域是唯一一個JVM規範中沒有規定OOMError的記憶體區域

JVM棧

JVM棧也是每個執行緒所私有的記憶體區域,隨著執行緒的建立而分配,執行緒的消亡而回收。JVM棧是描述Java方法執行的記憶體模型,每個java方法在執行的時候,都會建立一個棧幀(Stack Frame),其作用是用來儲存java方法中的區域性變數、操作棧、方法出口等資訊。每一個java方法從被呼叫到執行完畢的過程,都伴隨著對應的棧幀在JVM棧中的進棧和入棧。 
JVM規範中,該區域中有兩個異常狀況: 
-StackOverflowError:執行緒請求的棧深度超過了JVM棧所允許的棧深度

 
-OutOfMemoryError:如果JVM棧是動態可擴充套件的,在無法申請到足夠記憶體時,丟擲該異常

本地方法棧

本地方棧(Native Method Stack)是描述虛擬機器所用到的本地方法的記憶體模型,與JVM棧類似,也會丟擲StackOverflowError和OutOfMemoryError

Java堆

Java堆是java虛擬機器所管理的最大的記憶體區域,我們所說的垃圾回收,就是對該區域的記憶體進行回收,所以也叫GC堆。java堆是所有執行緒所公用的記憶體區域,程式中的物件、陣列都存放在該區域中。 
從記憶體回收的角度來看,由於現在的收集器所採用的都是分代收集演算法,所以java堆可以在進一步細分為:新生代和老年代,在新生代中,又可以劃分為Eden、From survivor、To survivor等空間。從記憶體分配的角度來看,java堆可能劃分為多個執行緒私有的分配快取區。 
java堆可以是物理上不連續但邏輯上要連續的記憶體空間,可以通過指定-Xmx(最大)、-Xms(最小)、-Xmn(新生代)等VM引數來設定java堆得大小。若有新物件申請記憶體空間但是java堆沒有足夠的記憶體分配時,會報OOMError。

方法區

方法區(Method Area)也是各執行緒共享的記憶體區域,用於儲存被虛擬機器載入的類資訊、常量、靜態常量、編譯後的程式碼等資料看,其還有一個別名叫Non-Heap(非堆),與java堆加以區分。在HotSpot虛擬機器上,也可叫做“永久代”(Permanent Generation),本質上不等價(HotSpot將GC收集擴充套件到了該區域)。 
若要對該區域進行GC,主要是回收常量池以及對型別的解除安裝。該區域在記憶體滿後也會丟擲OOMError異常。

執行時常量池

執行時常量池(Runtime Constant Pool)是方法區內的一部分,Class檔案中除了類的版本、欄位、方法、介面等描述資訊,還常量池表,用於存放編譯期間生成的葛總常量和符號引,這部分內容在類載入後存放執行時常量池這種(class如何載入會在後面章節提及)。執行時常量池具備動態性,java語言不單單在編譯期間會產生常量,在執行期間也可能將新的常量放入執行時常量池中,如開發人員用了String.intern()方法,之後的異常測試就是用該方法引起常量池丟擲OOMError。

直接記憶體

直接記憶體(Direct Memory)不是虛擬機器執行時資料區域的一部分,也不是JVM規範中定義的記憶體區域,但其也頻繁被使用。自JDK1.4後,引入了基於通道(Channel)與緩衝區的I/O方式,它可以使用native函式庫直接分配堆外記憶體,並通過java堆中的DirectByteBuffer物件操作該塊記憶體。避免了java堆和native堆之間的資料複製,提高了效能。 
本機直接記憶體不受java堆大小限制,但是受本機總記憶體大小和處理器的定址空間限制。在動態擴充套件時也會丟擲OOMError異常。

物件訪問

OutOfMemoryError異常

之前對虛擬機器執行時資料區域進行了描述,現在通過例項來驗證OOM異常發生的情況,可以進一步瞭解jvm各記憶體區域的儲存內容,及發生異常的情況。 
測試的java版本為:

java version “1.7.0_80” 
Java(TM) SE Runtime Environment (build 1.7.0_80-b15) 
Java HotSpot(TM) 64-Bit Server VM (build 24.80-b11, mixed mode)

Java堆溢位示例

之前已經提到,物件例項都儲存在java堆中,所以可以不斷的建立物件且保證物件不被GC就可以讓java堆溢位 
編寫如下程式碼清單:

public class HeapOOM {
    static class OOMObject{ 
    }

    public static void main(String[] args) {
        List<OOMObject> list = new  ArrayList<OOMObject>();
        while(true){
            list.add(new OOMObject());
        }
    }
}


並配置執行時的VM引數: 
VM引數配置
這裡-XX:UseParNewGC是指定用ParNew收集器,預設的是 
引數解釋: -Xms:20M:java堆最小20M 
-Xmx:20M:java堆最大20M(避免動態擴充套件) 
-XX:+HeapDumpOnOutOfMemoryError:開啟該選項,可以讓虛擬機器在丟擲OutOfMemoryError時Dump出當前記憶體堆轉存快照 
執行後,就會有如下輸出:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid3576.hprof ...
Heap dump file created [34233497 bytes in 0.301 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOf(Arrays.java:2760)
    at java.util.Arrays.copyOf(Arrays.java:2734)
    at java.util.ArrayList.ensureCapacity(ArrayList.java:167)
    at java.util.ArrayList.add(ArrayList.java:351)
    at study.hard.jjzhu.HeapOOM.main(HeapOOM.java:20)


JVM棧和本地方法棧溢位

之前有提過,HotSpot虛擬機器並沒有區分JVM和本地方法棧。在HotSpot虛擬機器中可以通過制定-Xss引數來指定棧的大小。 
編寫如下測試程式碼:

public class JavaVMStackSOF {
    private int stackLength = 1;
    public void stackLeak(){
        stackLength ++;
        //遞迴呼叫
        stackLeak();

    }

    public static void main(String[] args) throws Throwable {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try{
            oom.stackLeak();
        }catch(Throwable e){
            System.out.println("stack deep:" + oom.stackLength);
            throw e;
        }
    }
}


執行後就會報StackOverflowError異常:

stack deep:1007
Exception in thread "main" java.lang.StackOverflowError
    at study.hard.jjzhu.memory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:13)
    at study.hard.jjzhu.memory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:15)
    at study.hard.jjzhu.memory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:15)
    at study.hard.jjzhu.memory.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:15)


執行時常量池溢位

String.intern()方法可以向執行時常量池中新增內,所以要達到執行時常量池溢位,執行用本地方法intern()像池中不斷新增字串即可,可以同過-XX:PermSize -XX:MaxPermSize限制方法區大小。

public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        int i = 0 ;
        while(true){
            /* 
             * String.intern()是一個Native方法,作用是:如果字串常量池中已經包含一個等價於此String
             * 物件的字串,則返回常量池中這個字串的String物件,否則,將此String物件包含的字串新增到常量池中,
             * 並返回此String物件的引用
            */
            list.add(String.valueOf(i++).intern());
        }
    }
}
>執行結果:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
    at java.lang.String.intern(Native Method)
    at study.hard.jjzhu.memory.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:23)


本機直接記憶體溢位

本機直接記憶體可以通過 -XX:MaxDirectMemorySize指定記憶體大小,若不指定,則預設與java堆得最大值一樣。

public class DirectMemoryOOM {
    private static final int _1MB = 1024 * 1024;
    public static void main(String[] args) throws IllegalArgumentException, IllegalAccessException {
        Field unsafeField = Unsafe.class.getDeclaredFields()[0];
        unsafeField.setAccessible(true);
        Unsafe unsafe = (Unsafe)unsafeField.get(null);
        while(true){
            unsafe.allocateMemory(_1MB);
        }
    }
}
Exception in thread "main" java.lang.OutOfMemoryError
    at sun.misc.Unsafe.allocateMemory(Native Method)
    at study.hard.jjzhu.memory.DirectMemoryOOM.main(DirectMemoryOOM.java:21)

轉自:https://yq.aliyun.com/articles/71866?utm_content=m_17209