Java虛擬機器01——Java記憶體資料區域和記憶體溢位異常

llldddbbb發表於2019-03-31

執行時資料區域

Java虛擬機器在執行Java程式的過程中會把它所管理的記憶體劃分為若干個不同的資料區域。這些區域都有各自的用途,以及建立和銷燬的時間,有的區域隨著虛擬機器程式的啟動而存在,有些區域則依賴使用者執行緒的啟動和結束而建立和銷燬。根據《Java虛擬機器規範(Java SE 7版)》的規定,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關鍵字,但是,在虛擬機器中,建立物件包括如下流程:

類載入 --> 分配記憶體 --> 記憶體空間初始化零值 --> 物件頭設定 --> init初始化

  • 虛擬機器遇到一個new指令時,首先將去檢查這個指令的引數是否能在常量池中定位到一個類的引用,並且檢查這個符號引用代表的類是否已被載入、解析和初始化過。如果沒有,那必須先執行相應的類載入過程。
  • 在類載入通過後,接下來虛擬機器將為新生物件分配記憶體。物件所需的記憶體大小在載入後就確定。

分配記憶體的方式為:
  “指標碰撞”:在記憶體規整情況下,將指標向空閒空間挪動一段與物件大小相等的距離。
  “空閒列表”:在記憶體不規整情況下,虛擬機器維護一個記錄記憶體可用的列表,分配的時候從列表中找到一塊空間劃分給物件。
併發情況下的記憶體分配:
  同步:對分配記憶體空間的動作進行同步處理———採用CAS配上失敗重試的方式,保證更新操作的原子性
  本地執行緒分配緩衝(TLAB):把記憶體分配動作按照執行緒劃分在不同空間中。即每個執行緒在Java堆中預先分配一塊記憶體TLAB,只有TLAB用完並重新分配新的TLAB時才需要同步。

  • 將分配到的記憶體空間都初始化零值,如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());
        }
    }
}
複製程式碼

相關文章