天天看點

深入了解JVM虛拟機-JVM記憶體區域與記憶體溢出

1.JVM記憶體區域

深入了解JVM虛拟機-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中,已經徹底沒有了永久代,将方法區直接放在一個與堆不相連的本地記憶體區域,這個區域叫元空間。

深入了解JVM虛拟機-JVM記憶體區域與記憶體溢出

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本身不需要修改。

深入了解JVM虛拟機-JVM記憶體區域與記憶體溢出
2.3.2 直接指針通路

使用直接指針通路,那麼Java堆對象的布局中就必須考慮如何放置通路類型資料的相關資訊,而reference中存儲的直接就是對象位址。

直接指針通路方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷,由于對象的通路在Java中非常頻繁,是以這類開銷積少成多後也是一項非常可觀的執行成本。

深入了解JVM虛拟機-JVM記憶體區域與記憶體溢出
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{}
}
           

輸出結果如下:

深入了解JVM虛拟機-JVM記憶體區域與記憶體溢出

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;
        }
    }

}
           

運作結果:

深入了解JVM虛拟機-JVM記憶體區域與記憶體溢出

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
更改自旋鎖的自旋次數,使用這個參數必須先開啟自旋鎖
           
jvm