1. 程式人生 > >JVM記憶體結構劃分

JVM記憶體結構劃分

JVM記憶體結構劃分

  • JVM記憶體結構劃分
    • 資料區域劃分
      • 程式計數器
      • 虛擬機器棧
      • 本地方法棧
      • 方法區
        • 執行時常量池
        • StringTable
      • 直接記憶體
    • 建立新物件說明
      • 物件的建立
      • 物件的記憶體佈局
        • 物件頭
        • 例項資料
        • 對齊填充
      • 物件的訪問定位

資料區域劃分

執行時記憶體區域劃分:程式計數器、虛擬機器棧、本地方法棧、堆、方法區

程式計數器

  • 執行緒私有
  • 通過暫存器實現
  • 不會存在執行溢位

當前執行緒所執行的行號指示器,記住下一條JVM指令的執行地址

虛擬機器棧

  • 垃圾回收不涉及棧記憶體
  • 棧記憶體是執行緒私有的,可以理解為執行緒執行需要的記憶體空間
  • 棧由棧幀組成,每個棧幀代表一個方法執行時需要的記憶體(引數,區域性變數,返回地址)
  • 每個執行緒只能有一個活動棧幀,對應著當前正在執行的那個方法

棧記憶體分配過大隻能支撐一定的遞迴呼叫,並不會影響執行速度,還可能減少執行緒數量(因為實體記憶體是一定的)

本地方法棧

為執行本地方法時分配的記憶體(HotSpot把虛擬機器棧和本地方法棧合二為一了)

  • 有垃圾回收機制
  • 執行緒共享,需要考慮執行緒安全問題
  • 儲存的都是物件的例項(通過new關鍵字建立的物件)
  • 從記憶體分配的角度來說:堆中可以劃分出多個執行緒私有的分配緩衝區(TLAB),以提升物件分配時的效率
  • Java堆可以處於物理上不連續的記憶體空間,但在邏輯上應該視為連續的(但是對於比如陣列這種大物件,可能會要求連續的記憶體空間)

方法區

  • 執行緒共享區
  • 儲存已被虛擬機器載入的型別資訊,常量,靜態變數,即時編譯器編譯後的程式碼快取
  • 在虛擬機器啟動時被建立,邏輯上屬於堆的一部分(不同JVM實現的方式不同)
    • JDK1.6使用永久代(PerGen)作為方法區的實現
    • JDK1.8使用元空間(Metaspace)對方法區進行實現(包含Class ClassLoader 常量池三個部分,放在直接記憶體中)StringTable放在堆中(有助於垃圾回收管理)

使用場景:如Spring Mybatis使用的動態載入

執行時常量池

執行時常量池是方法區的一部分
二進位制位元組碼內容:類基本資訊\常量池表\類方法定義,包含了虛擬機器指令
其中,常量池表中存放編譯期間生成的各種字面量(比如各種基本資料型別)與符號引用(比如,類名\方法名\引數型別),這部分內容將在類載入後存放到方法區的執行時常量池中,並把符號地址變為真實地址

StringTable

類似於hashTable結構,不能自動擴容
常量池中的字串只是符號,第一次使用時才變為物件
利用串池機制,避免重複建立字元物件

  • 案例
    字串拼接的原理是編譯期優化
    字串拼接原理是StringBuilder(JDK1.8)
    使用intern()方法,主動將串池中還沒有的字串物件放入串池
// StringTable [ "a", "b" ,"ab" ]  hashtable 結構,不能擴容
public class Demo1_22 {
    // 常量池中的資訊,都會被載入到執行時常量池中, 這時 a b ab 都是常量池中的符號,還沒有變為 java 字串物件
    // ldc #2 會把 a 符號變為 "a" 字串物件
    // ldc #3 會把 b 符號變為 "b" 字串物件
    // ldc #4 會把 ab 符號變為 "ab" 字串物件

    public static void main(String[] args) {
        String s1 = "a"; // 懶惰的
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")
        String s5 = "a" + "b";  // javac 在編譯期間的優化,結果已經在編譯期確定為ab

        System.out.println(s3 == s5);
    }
}

JDK1.7以後,利用intern()方法,會將字串物件嘗試放入串池,如果有則並不會放入,如果沒有則放入串池, 會把串池中的物件返回;而JDK1.6呼叫intern()方法,是將物件拷貝一份到串池中,指向堆中的物件本身引用並不不改變


public class Demo1_23 {

    //  ["ab", "a", "b"]
    public static void main(String[] args) {
        demo1();
        demo2();
    }

    static void demo1() {
        // 串池中事前沒有"ab",intern()之後,s返回的是串池中的物件
        String s = new String("a") + new String("b");

        String s1 = s.intern();

        System.out.println(s == "ab");  // true
        System.out.println(s1 == "ab");     //true
    }

    static void demo2() {
        // 串池中事前已有"ab",s返回的仍是堆中的物件
        String x = "ab";
        String s = new String("a") + new String("b");

        // 堆  new String("a")   new String("b") new String("ab")
        String s2 = s.intern(); // 將這個字串物件嘗試放入串池,如果有則並不會放入,如果沒有則放入串池, 會把串池中的物件返回

        System.out.println( s2 == x);   // true
        System.out.println( s == x );   //false
    }
}
  • 位置
    JDK1.6時,StringTable放在元空間內,屬於永久代的位置,但是StringTable佔用記憶體容易觸發full gc耗時較久;JDK1.7以後將StringTable放在堆記憶體中,隨著記憶體佔用增大首先觸發minor gc,耗時較短.

直接記憶體

使用Native函式直接分配堆外記憶體,然後通過Java堆裡的DirectByteBuffer物件作為引用對這塊記憶體的引用進行操作.
原理說明:

使用Unsafe物件完成直接記憶體的分配和回收,回收時需要主動呼叫freeMemory方法
ByteBuffer的實現類內部使用了Cleaner(虛引用)來監測ByteBuffer(BB)物件,一旦BB物件被垃圾回收,會有ReferenceHandler執行緒通過Cleaner方法呼叫freeMemory來釋放記憶體

建立新物件說明

HotSpot虛擬機器在Java堆中物件分配、佈局和訪問的過程

物件的建立

  1. new位元組碼指令
    虛擬機器遇到new位元組碼指令時,首先檢查能否在常量池中定位到一個類的符號引用,並檢查該符號引用的來是否已被載入、解析和初始化。如果沒有,則執行相應的類載入過程。
    類載入檢查後,虛擬機器為新生物件分配記憶體

  2. 記憶體分配
    物件所需的記憶體大小在類載入過程中可以確定,在Java虛擬機器中為物件劃分記憶體時有兩種方式:指標碰撞、空閒列表
    指標碰撞: 利用一個指標作為已用記憶體和未用記憶體的分界點的指示器,記憶體分配就僅僅是指標的移動。優點在於不會造成記憶體碎片化,但是速度較慢
    空閒列表:虛擬機器維護一個記憶體使用記錄表,使用時,從空閒的記憶體區域直接劃分一塊足夠大的空間給物件例項。

  3. 記憶體分配的執行緒安全問題
    劃分可用空間後仍要考慮併發情況下對記憶體的使用,有兩種方式解決記憶體衝突的問題:CAS配上失敗重試、TLAB本地執行緒分配緩衝
    TLAB:把記憶體分配的動作按照執行緒劃分在不同的空間之中進行,即每個執行緒在Java堆中預先分配了一小塊記憶體空間

物件的記憶體佈局

物件在堆記憶體中的佈局可以劃分為三個部分:物件頭、例項資料、對齊填充

物件頭

物件頭中包含兩類資訊:Mark Word、型別指標

  • Mark Word
    儲存物件自身執行時資料,考慮到虛擬機器的空間效率,被設計成一個動態定義的資料結構,即根據物件的狀態複用自己的儲存空間(資料長度在32位和64位虛擬機器上分別為32個位元和64個位元)

  • 型別指標
    物件中指向它型別元資料的指標,Java虛擬機器通過這個指標來確定該物件是哪個類的例項(不是所有虛擬機器都必須在物件資料上保留型別指標)此外,如果物件是一個數組,物件頭中還必須擁有一塊記錄資料長度的資料

例項資料

即程式程式碼裡定義的各種型別的欄位內容,包括從父類繼承的或子類中定義的欄位。各類資料儲存是按照一定順序的(long/double、ints...),而寬度相同的欄位總是被分配到一起存放,所以父類中定義的變數可能會出現在子類之前。

對齊填充

佔位符,無特殊意義
HotSpot虛擬機器的自動記憶體管理系統要求物件的起始地址必須是8位元組的整倍數,若有些物件的物件頭和示例資料記憶體設計不是8的倍數,則需要利用佔位符來進行填充。

物件的訪問定位

Java程式通過reference資料操作對上的具體物件,主流的訪問方式有兩種:控制代碼、直接指標

  • 控制代碼
    Java堆中可能劃分出一塊記憶體作為控制代碼池。reference中儲存物件的控制代碼地址,控制代碼中包含物件的例項資料和型別資料的具體地址資訊。

  • 直接指標
    Java堆中物件的佈局需要考慮如何放置型別資料的相關資訊(如訪問資訊)。reference中儲存的直接就是物件地址,如果只訪問物件本身,就不要多一次間接訪問的開銷

優缺點

使用控制代碼訪問, reference資料只需關乎控制代碼地址,當物件被回收或移動後只需改變控制代碼中的例項資料指標,而reference本身不用修改
使用直接指標省去了一次指標定位的時間開銷,速度更快,由於Java中物件的訪問相當頻繁,所以效果可觀。
HotSpot使用直接指標的方式