Java虛擬機01——Java內存數據區域和內存溢出異常

運行時數據區域

Java虛擬機在執行Java程序的過程當中會把它所管理的內存劃分爲若干個不一樣的數據區域。這些區域都有各自的用途,以及建立和銷燬的時間,有的區域隨着虛擬機進程的啓動而存在,有些區域則依賴用戶線程的啓動和結束而創建和銷燬。根據《Java虛擬機規範(Java SE 7版)》的規定,Java虛擬機所管理的內存將會包括如下幾個運行時數據區域,以下圖所示:java

內存模型.jpeg

咱們能夠將上面的數據區域分爲線程獨有、線程共享及其餘三大區域:算法

1.1. 線程獨有的數據區域

1. 程序計數器(Program Counter Register)

  1. 當前線程所執行的字節碼的行號指示器。
  2. 用於選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復須要依賴這個計數

2. 虛擬機棧(Java Stack)

  • 位於線程私有的內存中,生命週期與線程相同。
  • 描述了Java方法執行的內存模型。
  • 方法執行時使用棧幀(Stack Frame)來存儲局部變量表、操做數棧、動態連接、方法出口等信息。
  • 若是線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverflowError異常。
  • 若是虛擬機棧能夠動態擴展,若是擴展時沒法申請到足夠的內存,就會拋出OutOfMemoryError異常。

3. 本地方法棧(Native Method Stack)

  • 與虛擬機棧相相似,區域在於本地方法棧爲虛擬機使用到的Native方法服務。
  • 能夠由虛擬機設計者本身實現。
  • 本地方法棧區域也會拋出StackOverflowErrorOutOfMemoryError異常

1.2. 線程共享的數據區域

1. Java堆(Heap)

  • 是Java虛擬機所管理內存中最大的一塊,在虛擬機啓動時建立。
  • 在Java虛擬機規範中的描述是:全部的對象實例以及數組都要在堆上分配。隨着JIT編譯器的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術致使某些對象並無分配在堆上。
  • Java GC工做的主要區域。現代收集器基本都採用分代收集算法,因此Java堆中還能夠細分爲新生代和老年代;再細緻一點的有Eden空間、From Survivor空間、To Survivor空間等。
  • 若是在堆中沒有內存完成實例分配,而且堆也沒法再擴展時,將會拋出OutOfMemoryError異常。

2. 方法區(Method Area)【Java8中去除了永久代 // TODO】

  • 用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。
  • 它有一個別名叫作Non-Heap(非堆),目的應該是與Java堆區分開來。
  • HotSpot虛擬機選擇把GC分代收集擴展至方法區,即便用永久代來實現方法區,所以也有人將此區域稱爲「永久代」;JDK 1.7的HotSpot中,已經把本來放在永久代的字符串常量池移出,並逐步改成採用Native Memory來實現方法區的規劃。
  • 根據Java虛擬機規範的規定,當方法區沒法知足內存分配需求時,將拋出OutOfMemoryError異常。

3. 運行時常量池(Runtime Constant Pool)

  • 運行時常量池是方法區的一部分。
  • 用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池中存放。
  • 當常量池沒法再申請到內存時會拋出OutOfMemoryError異常。

1.3. 其餘區域

直接內存(Direct Memory)

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

對象的建立

Java是一門面向對象的語言,在Java程序運行的過程當中無時不刻都有對象被建立。在語言層面,建立對象一般是一個new關鍵字,可是,在虛擬機中,建立對象包括以下流程:shell

類加載 --> 分配內存 --> 內存空間初始化零值 --> 對象頭設置 --> init初始化數組

  • 虛擬機遇到一個new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的引用,而且檢查這個符號引用表明的類是否已被加載、解析和初始化過。若是沒有,那必須先執行相應的類加載過程。
  • 在類加載經過後,接下來虛擬機將爲新生對象分配內存。對象所需的內存大小在加載後就肯定。

分配內存的方式爲:
  「指針碰撞」:在內存規整狀況下,將指針向空閒空間挪動一段與對象大小相等的距離。
  「空閒列表」:在內存不規整狀況下,虛擬機維護一個記錄內存可用的列表,分配的時候從列表中找到一塊空間劃分給對象。
併發狀況下的內存分配:
  同步:對分配內存空間的動做進行同步處理———採用CAS配上失敗重試的方式,保證更新操做的原子性
  本地線程分配緩衝(TLAB):把內存分配動做按照線程劃分在不一樣空間中。即每一個線程在Java堆中預先分配一塊內存TLAB,只有TLAB用完並從新分配新的TLAB時才須要同步。bash

  • 將分配到的內存空間都初始化零值,如int a,a默認爲0,若是使用TLAB,則這個工做提早到TLAB
  • 對象頭設置:對象是哪一個類的實例,對象的哈希碼,對象的GC分代年齡等信息。
  • 執行init方法,即執行程序定義的構造方法。

對象的內存佈局

在HotSpot虛擬機中,對象在內存中的存儲佈局能夠分爲3塊區域:對象頭(Header)、實例數據(Instance Data)和對其填充(Padding)數據結構

  • 對象頭:
    1.用於存儲對象自身運行時數據(Mark Word):哈希碼GC分代年齡鎖狀態標誌線程持有鎖偏向線程ID偏向時間戳等。被設計成非固定的數據結構,能服用存儲空間
    2.類型指針,即對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例。若是對象是Java數組,還必須有一塊記錄數組長度的數據。
  • 實例數據: 在程序代碼中定義的各類類型的字段內容。這部分的存儲會周到虛擬機分配策略的影響。HotSpot虛擬機的默認分配策略:相同寬度的字段老是被分配到一塊兒。父類中定義的變量會出如今子類前,子類中較窄的變量也可能會插入到父類變量的空隙之中。
  • 對齊填充: 僅僅起到佔位符的做用。HotSpot要求對象的起始地址必須是8字節的整數倍。當對象實例數據部分沒有對齊時,就須要經過對齊填充來補充。

對象的訪問定位

創建對象是爲了使用對象。咱們的Java程序須要經過棧上的reference數據來操做堆上的具體對象。目前主流的訪問方式有兩種:句柄和直接指針。併發

句柄:Java堆中會劃分出一塊內存來做爲句柄池,reference中存儲的就是對象的句柄地址。句柄中包含了對象實例數據與類型數據各自的具體地址。佈局

句柄.jpg

直接訪問:reference指針存儲的直接就是對象地址測試

直接地址.jpg

  使用句柄來訪問的最大好處就是reference中存儲的是穩定的句柄地址,在對象被移動(如垃圾收集時)時只會改變句柄中的實例數據指針,而reference自己不須要修改優化

  直接訪問最大的好處就是速度快。節省了一次指針定位的時間開銷。HotSpot虛擬機使用第二種方式進行對象的訪問。

OutofMemoryError異常實戰

堆溢出

-Xms 堆最小值 -Xmx 堆最大值 -XX:HeapDumpOnOutOfMemoryError能夠在虛擬機出現異常時將堆存儲快照

-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
複製代碼
public class HeadOOM {
    static class OOMObject {
    }
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<>();
        while (true) {
            list.add(new OOMObject());
        }
    }
}
複製代碼

運行結果:

image.png

虛擬機棧和本地方法棧溢出

-Xss 設置棧的大小

  • 若是線程請求的棧深度大於虛擬機所容許的最大深度,拋出StackOverflow異常
  • 若是虛擬機在擴展棧時沒法申請到足夠的空間,則拋出OutOfMemoryError異常
-Xss228k
複製代碼
public class JavaVMStackSOF {
    private int stackLength = 1;
    public void stackLeak() {
        stackLength ++;
        stackLeak();
    }

    public static void main(String[] args) {
        JavaVMStackSOF stackSOF = new JavaVMStackSOF();
        stackSOF.stackLeak();
    }
}
複製代碼

運行結果:

image.png

實驗結果代表,在單線程下,當內存沒法分配的時候,虛擬機拋出的都是StackOverflow異常

測試:建立線程致使內存溢出異常

public class JavaVMStackOOM {
    private void dontStop() {
        while (true) {

        }
    }

     public void stackLeakByThread() {
        while (true) {
            new Thread(() -> {
                dontStop();
            }).start();
        }
     }

    public static void main(String[] args) {
        JavaVMStackOOM javaVMStackOOM = new JavaVMStackOOM();
        javaVMStackOOM.stackLeakByThread();
    }
}
複製代碼

方法區和運行時常量池溢出

String.intern()方法返回的是常量池中的對象,若是池中沒有對象,則建立對象返回引用

在JDK 1.6及以前的版本中,因爲常量池分配在永久代內,咱們能夠經過-XX:PermSize和-XX:MaxPermSize限制方法區大小,從而間接限制其中常量池的容量,測試代碼:

public class RuntimeConstantPoolOOM {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        int i = 0;
        while (true) {
            list.add(String.valueOf(i++).intern());
        }
    }
}
複製代碼
相關文章
相關標籤/搜索