JVM系列以內存結構與內存溢出異常

概述

對於全部的java開發人員來講,瞭解和熟悉JVM的內存結構是很是重要的,不管是對於開發時的一些參數說的設置仍是維護時的異常排查都有很大的幫助,下面咱們先來了解下JVM內存結構java

內存結構

從JDK1.8開始,JVM內存結構發生一點變化多線程

JDK1.8的JVM內存結構圖

JDK1.8

JDK1.8以前的JVM內存結構圖

JDK1.7
從上面兩圖中咱們能夠發現,內存結構確實有點變化,但其實變化其實很小:

  • 能夠發現就是JDK1.8以後去掉了方法區,取而代之的是MetaSpace區域;
  • 將運行常量池從方法區中移到了堆中.
    接下來咱們就來一一分析一下每個結構區(這裏咱們以JDK1.8以前的來主要分析)

程序計數器

程序計數器(Program Counter Register)是一塊較小的內存空間,主要能夠看做當前線程所執行的字節碼的行號指示器。它會指示出下一條將要執行指令的地址。
ide

  • 線程私有的內存;
  • 若是線程正在執行的是一個java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;
  • 若是正在執行的一個Native方法,這個計數器則會爲空;
  • 惟一一個再java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域。

Java虛擬機棧

  • 線程私有的,生命週期與線程相同
  • 虛擬機棧描述的java方法執行的內存模型:每一個方法被執行的時候都會同時建立一個棧幀用於存儲局部變量表、操做棧、動態連接、方法出口等信息。
  • 局部變量表中存放了編譯期可知的各類基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,他不等同於對象自己)和returnAddress類型(指向了一條字節碼指令的地址);
  • Java虛擬機規範中,對這個區域規定了兩種異常狀況:若是線程請求的棧深度大於虛擬機所容許的棧深度,將拋出StackOverflowError異常;可是通常狀況下如今都容許動態擴展,當擴展時沒法申請到足夠的內存時會拋出OutOfMemoryError異常。
  • 若是單線程工做環境下,調用某一個方法並在該方法中遞歸調用本身,會拋出StackOverFlowError,如如下代碼
// VM Args: -Xss128k (將虛擬機棧設爲128k大小)
public class Test {
    public int length = 0;
    public void stackLeak() {
        length++;
        stackLeak();
    }
    //運行下本代碼會出先StackOverflowError
    public static void main(String[] args) {
        Test test = new Test();
        try {
            test.stackLeak();
        } catch(Throwable e) {
            System.out.println("stack length:" + test.length);
            throw e;
        }
    }
}
複製代碼
  • 想要出現OutOfMemoryError的話須要在多線程的環境下,不停的開啓線程,纔有可能會出現OutOfMemoryError異常,如如下代碼
// VM Args:-Xss2M
public class JavaVMStackOOM {
    private void dontStop() {
        while (true) {
            
        }
    }
    //運行本代碼會出現OutOfMemoryError異常,可是運行此代碼可能會致使Windows直接掛了,因此得注意,記得保存本身手頭的工做再決定是否是運行
    public static void main(String[] args) {
        while(true) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }
}
複製代碼

本地方法棧

  • 本地方法棧與虛擬機棧發揮的做用很是類似,一個是爲執行java方法服務的,一個是爲執行native方法服務的。
  • 本地方法棧也可能拋出StackOverflowError和OutOfMemoryError異常

Java堆

  • 對於大多數應用來講,java堆是java虛擬機所管理的內存中最大的一塊
  • java堆是全部線程共享的一塊內存,在虛擬機建立時啓動
  • 幾乎全部的對象實例都放在java堆中(jdk1.8以後運行時常量池也放到java堆中了)
  • java堆是垃圾收集器管理的主要區域,能夠分爲新生代和老年代,而新生代又能夠分爲Eden區、From Survivor區、To Survivor區等
  • Java堆能夠經過Xmx(最大堆大小)和Xms(最小堆大小)兩個參數來指定堆的大小
  • Java堆能夠處理物理上不連續的內存空間中,只要邏輯上是連續的便可
  • 若是在Java堆中沒存完成實例分配,而且堆也沒法進行擴展時,將會拋出OutOfMemoryError異常
  • 可想而知,若是咱們須要測試Java堆的OutOfMemoryError異常的話,只須要在堆上不停的分配對象實例就能夠了,以下代碼
//VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
public class HeapOOM {
    static class OOMObject {
        
    }
    //運行這段代碼會出現OutOfMemoryError異常
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<>();
        while (true) {
            list.add(new OOMObject());
        }
    }
}
複製代碼

方法區

  • 與Java堆同樣,是全部線程共享的內存區域;
  • 存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據(也會被叫作永久代【Permanent Generation】)。
  • jdk1.8以前運行時常量池是在方法區分配的,1.8以後方法區被取消了,而後新增了一個元數據區,同時也將運行時常量池放到了堆中
  • 根據java虛擬機規範的規定,當方法區沒法知足內存分配需求時,將拋出OutOfMemoryError異常。
  • 下面展現一下如何測試出方法區的OutOfMemoryError異常
//VM args : -XX:PermSize=10M -XX:MaxPermSize=10M
public class JavaMethodAreaOOM {
    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback(new MethodInterceptor() {
                public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
                    return proxy.invokeSuper(obj, args);
                }
            });
            enhancer.create();
        }
    }
}
複製代碼

運行時常量池

  • 在咱們上面提到的,運行時常量池時方法區的一部分,用於存放編譯期生成的各類字面量和符號引用
  • 在運行期間將新的常量放進常量池能夠經過String類的intern()方法;
  • 運行時常量池沒法再申請到內存會拋出OutOfMemoryError異常;
  • 因此爲了測試運行常量池的OutOfMemoryError異常,咱們應該只須要一直往常量池裏面放常量,按理來講應該就會出現該異常了,以下代碼所示:
//VM args : -XX:PermSize=10m -XX:MaxPermSize=10m
public class RuntimeConstantPoolOOM {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}
複製代碼

直接內存

  • 直接內存不是虛擬機運行時數據區的一部分,也不是虛擬機規範中定義的內存區域,可是這部份內存也會被頻繁使用,並且也可能致使OutOfMemoryError異常的出現

對象訪問

  • Java中對象訪問方式主要有兩種:句柄訪問和直接指針
  • 使用句柄訪問方式,Java堆中會劃分出一塊內存來做爲句柄池,reference中存儲的是對象的句柄地址,而句柄中包含了對象實例數據和類型數據的各自的具體地址信息,以下圖所示:

句柄訪問

  • 使用直接指針方式,Java堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息,reference中直接存儲的就是對象的地址,以下圖所示:

直接方式

  • 這兩種對象的訪問方式各有優點,使用句柄訪問方式的最大好處就是reference中存的是對象句柄的地址,當對象被移動時只會改變句柄中的實例數據指針,而reference自己不須要被修改;使用直接指針訪問的方式最大好處就是快,由於少了一次尋址的過程,因爲對象訪問在Java中是很是頻繁的,全部這類開銷聚沙成塔也是很是可觀的執行成本。Sun HotSpot虛擬機就是採用的直接指針的方式來訪問對象的。

總結

本文主要總結了下JVM的內存結構以及每一個結構區相應的存放內容和溢出異常,不少只是借鑑於《深刻理解Java虛擬機++JVM高級特性與最佳實踐這本書》,若是想更好的瞭解的朋友也能夠去看看這本書,寫的很好很詳細!佈局

相關文章
相關標籤/搜索