深入理解JVM虛擬機器-JVM記憶體區域與記憶體溢位

王廷駿發表於2019-05-09

1.JVM記憶體區域

在這裡插入圖片描述

1.1 程式計數器

程式計數器是一塊較小的記憶體空間,它可以看作是當前執行緒所執行的位元組碼的行號指示器。在虛擬機器中,位元組碼直譯器工作時就是通過改變計數器值來選取下一條執行的指令,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。 在任何時刻,一個處理器(核心)都只會執行一條執行緒中的指令。為了執行緒切換後能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各條執行緒之間計數器互不影響,獨立儲存,這類記憶體區域為執行緒私有的記憶體此記憶體區域是唯一一個在Java虛擬機器規範中沒有規定任何OutOfMemoryError情況的區域。

1.2 java虛擬機器棧

Java虛擬機器棧也是執行緒私有的,它的生命週期與執行緒相同。 每個方法在執行時都會建立一個棧幀,用於儲存區域性變數表、運算元棧、動態連結、方法出口 等資訊。一個方法從呼叫到執行完成就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。 虛擬機器棧中的區域性變數表存放著編譯期可知的各種基本資料型別(boolean、byte、char、short、int、float、long、double),物件引用(reference型別),returnAddress型別(指向了一條位元組碼指令的地址)。 StackOverflowError異常:執行緒請求的棧深度大於虛擬機器所允許的深度。 OutOfMemoryError異常:虛擬機器棧可以動態擴充套件無法申請到足夠的記憶體。

1.3 本地方法棧

本地方法棧與虛擬機器棧的作用是類似的,只不過虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的Native方法服務。 有的虛擬機器(例如Sun HotSpot虛擬機器)直接就把本地方法棧和虛擬機器棧合二為一。

1.4 java 堆

Java堆(Java Heap)是Java虛擬機器所管理的記憶體中最大的一塊。Java堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。堆的唯一目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體。 Java堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“GC堆”。從記憶體回收的角度來看,由於現在收集器基本都採用分代收集演算法,所以Java堆中還可以細分為:新生代和老年代;再細緻一點的有Eden空間、From Survivor空間、To Survivor空間等。 OutOfMemoryError異常:堆中沒有記憶體完成例項分配,並且堆也無法再擴充套件。

1.5 方法區

方法區(Method Area)與Java堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。 在Hotspot虛擬機器上,很多人更願意把方法區稱為“永久代”。 方法區和Java堆一樣不需要連續的記憶體和可以選擇固定大小或者可擴充套件外,還可以選擇不實現垃圾收集。相對來說垃圾收集行為在這個區域是比較少出現的,但並非資料進入了方法區就如永久代的名字一樣“永久”存在了。這區域的記憶體回收目標主要是針對常量池的回收和對型別的解除安裝。

1.5.1 jdk6-8中方法區的不同

在jdk7之前,HotSpot虛擬機器中將GC分代收集擴充套件到了方法區,使用永久代來實現了方法區。這個區域的記憶體回收目標主要是針對常量池的回收和對型別的解除安裝,但是在之後的HotSpot虛擬機器實現中,逐漸開始將方法區從永久代移除。jdk7中已經將執行時常量池從永久代移除,在Java堆(Heap)中開闢了一塊區域存放執行時常量池。而在jdk8中,已經徹底沒有了永久代,將方法區直接放在一個與堆不相連的本地記憶體區域,這個區域叫元空間。

在這裡插入圖片描述

Metaspace(元空間) 元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機器中,而是使用本地記憶體。因此,預設情況下,元空間的大小僅受本地記憶體限制,但可以通過引數來指定元空間的大小。

1.6 執行時常量池

執行時常量池(Runtime Constant Pool)是方法區的一部分。Class檔案中除了有類的欄位,版本等資訊描述外,還有一項資訊是常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類載入後進入方法區的執行時常量池中存放。

2. HotSpot虛擬機器物件探祕

2.1 物件的建立

  1. 當虛擬機器遇到一條new指令時,首先將去檢查這個指令的引數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被載入、解析和初始化過。如果沒有,那必須先執行相應的類載入過程,
  2. 接下來虛擬機器將為新生物件分配記憶體,物件所需記憶體的大小在類載入完成後便可以完全確定。物件記憶體分配分為兩種: 指標碰撞:設Java堆中記憶體是絕對規整的,所有用過的記憶體都放在一邊,空閒的記憶體放在另一邊,中間放著一個指標作為分界點的指示器,那所分配記憶體就僅僅是把那個指標向空閒空間那邊挪動一段與物件大小相等的距離。 空閒列表:如果Java堆中的記憶體並不是規整的,已使用的記憶體和空閒的記憶體相互交錯,那就沒有辦法簡單地進行指標碰撞了,虛擬機器就必須維護一個列表,記錄上哪些記憶體塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給物件例項,並更新列表上的記錄。

選擇哪種分配方式由Java堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。

物件建立在虛擬機器中是非常頻繁的行為,在併發情況下也並不是執行緒安全的。解決這個問題有兩種方案:一種是對分配記憶體空間的動作進行同步處理——實際上虛擬機器採用CAS配上失敗重試的方式保證更新操作的原子性。另一種是把記憶體分配的動作按照執行緒劃分在不同的空間之中進行,即每個執行緒在Java堆中預先分配一小塊記憶體,稱為本地執行緒分配緩衝(Thread Local Allocation Buffer,TLAB)。 3. 接下來虛擬機器要對物件進行必要的設定,,如物件的雜湊碼,GC的分代年齡等,這些資訊存放在物件的物件頭中。 4. 當上面的步驟走完後,從虛擬機器的角度看一個新的物件已經產生了,但從java程式來看,這個物件才剛剛開始----init方法還沒有執行。 當執行new指令之後會接著執行init方法,把物件按照程式設計師的意願進行初始化,這樣一個真正可用的物件才算完全產生出來。

2.2 物件的記憶體佈局

在HotSpot虛擬機器中,物件在記憶體中儲存的佈局可以分為3塊區域:物件頭(Header)、例項資料(Instance Data)和對齊填充(Padding)。

2.2.1 物件頭

物件頭包括兩部分資訊,第一部分用於儲存物件自身的執行時資料,鎖狀態標誌等。另外一部分是型別指標,即物件指向它的類後設資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。

2.2.2 例項資料

例項資料部分是物件真正儲存的有效資訊,也是在程式程式碼中所定義的各種型別的欄位內容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。

2.2.3 對齊填充

對齊填充並不是必然存在的,僅僅起著佔位符的作用。由於HotSpot VM的自動記憶體管理系統要求物件起始地址必須是8位元組的整數倍,也就是說物件的大小必須是8位元組的整數倍,不足就由物件填充來補齊。

2.3 物件的訪問定位

我們的Java程式需要通過棧上的reference資料來操作堆上的具體物件。由於reference型別在Java虛擬機器規範中只規定了一個指向物件的引用,並沒有定義這個引用應該通過何種方式去定位、訪問堆中的物件的具體位置,所以物件訪問方式也是取決於虛擬機器實現而定的。 目前主流的訪問方式有使用控制程式碼和直接指標兩種。

2.3.1 控制程式碼訪問

使用控制程式碼訪問,Java堆中將會劃分出一塊記憶體來作為控制程式碼池,reference中儲存的就是物件的控制程式碼地址,而控制程式碼中包含了物件例項資料與型別資料各自的具體地址資訊。 控制程式碼來訪問的最大好處就是reference中儲存的是穩定的控制程式碼地址,在物件被移動(垃圾收集時移動物件是非常普遍的行為)時只會改變控制程式碼中的例項資料指標,而reference本身不需要修改。

在這裡插入圖片描述

2.3.2 直接指標訪問

使用直接指標訪問,那麼Java堆物件的佈局中就必須考慮如何放置訪問型別資料的相關資訊,而reference中儲存的直接就是物件地址。 直接指標訪問方式的最大好處就是速度更快,它節省了一次指標定位的時間開銷,由於物件的訪問在Java中非常頻繁,因此這類開銷積少成多後也是一項非常可觀的執行成本。

在這裡插入圖片描述

Hotpost虛擬機器使用的是直接指標訪問的方式。

3 記憶體溢位

記憶體溢位 out of memory,是指程式在申請記憶體時,沒有足夠的記憶體空間供其使用,出現out of memory;比如申請了一個integer,但給它存了long才能存下的數,那就是記憶體溢位。

3.1 Java堆溢位

Java堆用於儲存物件例項,只要不斷地建立物件,並且保證GC Roots到物件之間有可達路徑來避免垃圾回收機制清除這些物件,那麼在物件數量到達最大堆的容量限制後就會產生記憶體溢位異常。 JVM得擴充引數在註釋中。

public class HeapOOM {

    /**
     *java堆溢位
     * 
     * -verbose:gc -Xms20M -Xmx20M -XX:+HeapDumpOnOutOfMemoryError
     */
    public static void main(String[] args) throws InterruptedException {
        ArrayList<OOMObject> oomObjects = new ArrayList<>();
        while (true){
            oomObjects.add(new OOMObject());
        }
    }

    static class OOMObject{}
}
複製程式碼

輸出結果如下:

在這裡插入圖片描述
Java堆記憶體的OOM異常是實際應用中常見的記憶體溢位異常情況。當出現Java堆記憶體溢位時,異常堆疊資訊“java.lang.OutOfMemoryError”會跟著進一步提示“Java heap space”。

3.2 虛擬機器棧溢位

關於虛擬機器棧在Java虛擬機器規範中描述了兩種異常: 如果執行緒請求的棧深度大於虛擬機器所允許的最大深度,將丟擲StackOverflowError異常。 如果虛擬機器在擴充套件棧時無法申請到足夠的記憶體空間,則丟擲OutOfMemoryError異常。

public class JavaVmStackSOF {

    private int stackLength = 1;
    public void stackLeak(){
        stackLength++;
        stackLeak();
    }

    /** 
     *虛擬機器棧溢位
     * -Xss128k
     */
    public static void main(String[] args) {
        JavaVmStackSOF javaVmStackSOF = new JavaVmStackSOF();
        try{
            javaVmStackSOF.stackLeak();
        }catch (Throwable t){
            System.out.println("stack length:"+javaVmStackSOF.stackLength);
            throw t;
        }
    }

}
複製程式碼

執行結果:

在這裡插入圖片描述

4 常用JVM命令引數

-Xms20M
表示設定JVM啟動記憶體的最小值為20M,必須以M為單位

-Xmx20M
表示設定JVM啟動記憶體的最大值為20M,必須以M為單位。將-Xmx和-Xms設定為一樣可以避免JVM記憶體自動擴充套件。大的專案-Xmx和-Xms一般都要設定到10G、20G甚至還要高

-verbose:gc
表示輸出虛擬機器中GC的詳細情況

-Xss128k
表示可以設定虛擬機器棧的大小為128k

-Xoss128k
表示設定本地方法棧的大小為128k。不過HotSpot並不區分虛擬機器棧和本地方法棧,因此對於HotSpot來說這個引數是無效的

-XX:PermSize=10M
表示JVM初始分配的永久代的容量,必須以M為單位

-XX:MaxPermSize=10M
表示JVM允許分配的永久代的最大容量,必須以M為單位,大部分情況下這個引數預設為64M

-Xnoclassgc
表示關閉JVM對類的垃圾回收

-XX:+TraceClassLoading
表示檢視類的載入資訊

-XX:+TraceClassUnLoading
表示檢視類的解除安裝資訊

-XX:NewRatio=4
表示設定年輕代:老年代的大小比值為1:4,這意味著年輕代佔整個堆的1/5

-XX:SurvivorRatio=8
表示設定2個Survivor區:1個Eden區的大小比值為2:8,這意味著Survivor區佔整個年輕代的1/5,這個引數預設為8

-Xmn20M
表示設定年輕代的大小為20M

-XX:+HeapDumpOnOutOfMemoryError
表示可以讓虛擬機器在出現記憶體溢位異常時Dump出當前的堆記憶體轉儲快照

-XX:+UseG1GC
表示讓JVM使用G1垃圾收集器

-XX:+PrintGCDetails
表示在控制檯上列印出GC具體細節

-XX:+PrintGC
表示在控制檯上列印出GC資訊

-XX:PretenureSizeThreshold=3145728
表示物件大於3145728(3M)時直接進入老年代分配,這裡只能以位元組作為單位

-XX:MaxTenuringThreshold=
表示物件年齡大於1,自動進入老年代

-XX:CompileThreshold=1000
表示一個方法被呼叫1000次之後,會被認為是熱點程式碼,並觸發即時編譯

-XX:+PrintHeapAtGC
表示可以看到每次GC前後堆記憶體佈局

-XX:+PrintTLAB
表示可以看到TLAB的使用情況

-XX:+UseSpining
開啟自旋鎖

-XX:PreBlockSpin
更改自旋鎖的自旋次數,使用這個引數必須先開啟自旋鎖
複製程式碼

相關文章