《Understanding the JVM》讀書筆記之一——JVM內存模型

一、JVM內存模型——概念說明

  1. 程序計數器
    • 程序計數器:內存佔用很小,是當前線程所執行的字節碼的行號指示器,每一個線程都需要一個獨立的程序計數器。
    • 如果該線程正在執行java方法,則這個計數器記錄的是正在執行的虛擬機字節碼指令地址;如果正在執行的是native方法,則這個計數器的值爲空。
    • *程序計數器是唯一一個再Java虛擬機規範中沒有outofmemory情況的區域
  

  2. 虛擬機棧
    • 虛擬機棧內存中的數據與線程的生命週期相同。
    • 在每個線程中,每個方法執行的同時都會在這裏(虛擬機棧)創建一個棧幀,每一個方法從調用到執行完成都對應一個棧幀的入棧和出棧過程。
    • 當所有棧幀都出棧則代表該線程的所有方法都執行完成,線程執行完畢
    • 每個棧幀都包括了局部變量表、操作數棧、動態鏈接、方法返回地址和一些額外的附加信息。
    • **異常:如果線程請求的棧深度 > jvm允許的最大棧深度,拋StackOverflowError;如果虛擬機棧可以動態擴展,當擴展時無法申請到足夠的內存,拋OutOfMemoryError;
  

  3. 本地方法棧
• 當虛擬機執行Native方法時使用本地方法棧,功能和虛擬機棧相似
• 在Sun的HotSpot虛擬機中,將本地方法棧和虛擬機棧合併了

  4. 堆
    • 用於存放(幾乎)所有的對象實例(不是絕對,在棧上分配、標量替換技術上不是),也被稱爲GC堆(因爲是垃圾收集器管理的主要區域)
    • Java堆內存只要邏輯上連續即可,物理上可以不連續
    • 當堆中已經沒有內存用於存放新的對象時(堆也無法擴展時),會拋出OutOfMemoryError
    • 可以通過-Xmx和-Xms控制堆內存大小

  5. 方法區
    • 用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯的代碼等數據。
    • 一般來講,不會對這個區域進行垃圾回收(GC),因爲回收的效率不高,只能回收常量池(jdk1.7以前)和進行類型卸載,但類型卸載的條件相當高
    • 當方法區無法滿足內存分配的需求時會拋出OutOfMemoryError

  6. 常量池

    • 在jdk1.7之前,運行時常量池包含字符串常量池,保存在永久代(方法區)中。
    • 在jdk1.7之後,運行時常量池中的字符串常量池被存放在堆內存中,運行時常量池中的其他內容仍然放在方法區中。

    • 在jdk1.8之前,方法區稱爲“永久代”,在1.8之後,用元空間取代了永久代,此時字符串常量池還在堆中,運行時常量池還在方法區。

  7. 直接內存
    • 這部分不是虛擬機運行時數據中的一部分
    • 在NIO包中的通道Channel中會使用Native函數庫直接申請堆外內存,這樣能顯著提升性能
    • 在通過-Xmx等參數分配虛擬機內存時,需要考慮直接內存,不要出現jvm各個區域內存綜合大於物理內存的情況,這種情況會拋出OutOfMemoryError

 

二、對象的創建過程

 

當JVM檢測到new語句時,會按照如下過程進行對象的創建:

  1. 檢查這個指令的參數是否能在常量池中定位到類的符號或引用,並檢查這個對象代表的類是否已經被加載、解析和初始化?如果沒有,需要先執行類的初始化過程;
  2. 類加載完成後,對象所需要的內存大小已經確定,這時候虛擬機將爲新創建的對象分配內存。有兩種分配方式:“指針碰撞”和“空閒列表”,具體選擇哪一種方式由所採用的垃圾收集器是否具有壓縮整理功能決定。
  3. 內存分配完成後,虛擬機將分配到的內存空間都初始化爲0(不包括對象頭),如果是TLAB,這一操作會提前到TLAB分配時執行。
  4. 虛擬機對對象進行必要的設置,包括:屬於哪個類、元數據信息位置、哈希碼、GC分代年齡等,這些數據都保存在對象頭中。
  5. 以上工作完成後,對虛擬機來說一個新的對象已經產上了,但對於程序來說,對象的創建纔剛剛開始。
接着執行<init>方法,將對象初始化。

整個過程總結如圖:

  

三、對象在內存中的佈局

對象在內存中的佈局可以分爲3塊:對象頭,實例數據,對齊填充;
  1. 對象頭(Header),包含兩部分
    • 第一部分用於存儲對象自身的運行時數據,包括:哈希碼、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等。
    • 第一部分數據長度爲64bit(在32位虛擬機中爲32bit),官方稱爲MarkWord。
    • 第二部分是類型指針(指向類數據的指針),通過這個指針來確定這個對象屬於哪個類實例。
    • 如果對象是一個數組,在對象頭中還需要一塊用於記錄數組長度數據的空間。
  2. 實例數據(InstanceData)保存對象真正存儲的有效信息,也就是在代碼中定義的各種類型的字段內容(包括在父類繼承的和類本身的)。
    • 數據的存儲順序受到虛擬機分配策略參數FieldsAllocationStyle和字段在java源碼中的定義順序影響(longs/doubles,ints,shorts/chars,bytes/booleans,oops)
    • 一般在父類中定義的參數會在前邊存儲,如果參數CompactFields參數設置爲true,則子類中長度較小的變量可能會插入到父類的變量空隙中。
  3. 對齊填充(Padding),這部分不是必要的。
    • 因爲在hotspot虛擬機中要求,對象的起始地址必須是8字節的整數倍,而對象頭正好是8字節的整數倍,因此,在當對象實例數據部分沒有對齊時,用對其填充進行補齊。

內容總結如圖:

  

四、對象的訪問定位
  Java程序通過棧上的reference數據來操作堆上的具體對象,對象的具體訪問方式取決於虛擬機的實現,主流的訪問方式有句柄訪問和直接指針訪問兩種,hotspot虛擬機使用直接指針訪問的方式。
  • 句柄訪問:在Java堆中劃分出一塊區域作爲句柄池,本地變量表中存儲的實際上是對象的句柄池地址,而句柄中記錄了對象的實例數據和類型數據的具體地址。
  • 直接指針訪問:reference中存儲的就是對象的直接地址。

  句柄訪問的好處是:reference中存儲的是穩定的句柄地址,在對象被移動時,只改變句柄中的實際地址,reference本身不需要修改。
  直接指針訪問速度塊,節省了一次指針定位的時間,在對象的數量多時會節省大量時間

END====解釋
  內存分配方式:

    • 指針碰撞——當Java堆中的內存是規整的,所有有對象的內存放在一邊,空閒的內存放在另一邊,中間通過一個指針作爲分界點的指示器,那麼這時爲對象分配內存的操作就是把指針向空閒的一端移動對應的距離即可。
    • 空閒列表——當Java堆中的內存是不規整的,已使用和未使用的內存交叉在一起,這時候虛擬機必須維護一個列表,來記錄那些內存時可用的那些內存是不可用的,在分配對象內存時直接在表中選擇一個足夠大的內存劃分給該實例,並維護列表即可。
    • 在使用Serial、ParNew等帶有Compact功能的收集器時系統採用的是“指針碰撞”,在使用CMS的收集器通常採用“空閒列表”的方式(CMS基於Mark-Sweep算法)。
    • 在分配內存空間時還需要考慮另外一個因素:在併發的情況下,可能正在給A對象分配內存,指針或列表還沒來的及修改,對象B又同時使用了本來分配給A的內存位置,解決這個問題有兩種方案。
    方案一:虛擬機採用CAS+失敗重試的方式來保證分配內存操作的原子性;
    方案二:把內存分配的動作按照線程劃分在不同的空間中進行,即爲每個線程在堆中分配一小塊內存(稱爲:本地線程分配緩衝ThreadLocalAllocationBuffer TLAB),在TLAB內存用完時才需要同步鎖定。可以通過參數-XX:+/-UseTLAB參數來設置TLAB的大小

  TLAB——Thread Local Allocation Buffer(線程本地分配緩衝區)
    1. 如果設置了虛擬機參數 -XX:UseTLAB,在線程初始化時,同時也會申請一塊指定大小的內存,只給當前線程使用,這樣每個線程都單獨擁有一個空間,如果需要分配內存,就在自己的空間上分配,這樣就不存在競爭的情況,可以大大提升分配效率。
    2. TLAB空間的內存非常小,缺省情況下僅佔有整個Eden空間的1%,也可以通過選項-XX:TLABWasteTargetPercent設置TLAB空間所佔用Eden空間的百分比大小。
    3. TLAB簡單來說本質上就是三個指針:start,top 和end,其中start 和end 是佔位用的,標識出Eden 裏被這個TLAB 所管理的區域,
而top 就是裏面的分配指針。

  關於Java堆內存的說明
  由於現代垃圾收集器主要基於分代收集算法實現,因此,堆內存可以劃分爲:新生代和老年代。更詳細的可以將新生代劃分爲Eden空間、FromSurvivor空間、ToSurvivor空間。
  從內存分配的角度看,線程共享的Java堆中可能劃分出多個線程私有的分配緩衝區。但是,無論如何劃分、目的都是爲了更好的回收內存或者更快的分配內存

 

****文章內容出自《Understanding the JVM》一書,部分內容來源於網絡,部分內容爲作者原創

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章