1. 程式人生 > 實用技巧 >JVM——Java物件是如何建立、儲存和訪問的?

JVM——Java物件是如何建立、儲存和訪問的?

關注微信公眾號:CodingTechWork,一起學習進步。

引言

  Java程式設計師都知道如何建立物件,不就是一個Person person = new Person()的語句就解決了麼?然而,我們只知道new,卻對於底層如何實現物件的建立、如何儲存到記憶體中去、又如何被訪問的知之甚少。

物件的建立

流程圖

建立流程

  1. Java程式new一個物件。
  2. 虛擬機器遇到一條new指令時,首先檢查這個指令的引數是否能在常量池中定位到一個類的符號引用,且檢查該符號引用代表的類是否已被載入、解析和初始化過。若沒有,需先進行相應的類載入過程。
  3. 在類載入檢查通過後,虛擬機器將為新生物件分配記憶體。(物件在記憶體中所需要的大小在類載入完成後就確定了)
  4. 記憶體分配完之後,虛擬機器需要將分配到的記憶體空間初始化為零值(不包括物件頭)。保證了物件的例項欄位在Java程式碼中可以不賦初始值就直接使用,可以訪問對應的零值。(對應準備階段)
  5. 虛擬機器對物件進行必要的設定(物件頭的設定)。如這個物件是哪個類的例項、如何找到類的元資料資訊、物件雜湊碼、物件的GC分代年齡等資訊。
  6. 以上虛擬機器中新物件產生,對應到Java程式還需要繼續執行<init>方法,將物件在程式中進行初始化。

記憶體空間分配方式

  為物件分配空間就是從Java堆中劃分出一塊確定大小的記憶體給新生物件,考慮符合劃分可用空間的兩種方式:“指標碰撞”和“空閒列表”

  • 指標碰撞
    :若Java堆中記憶體是絕對規整的,所有用過的記憶體都放在一邊,空閒的記憶體放在另一邊,中間放著一個指標作為分界點的指示器,所分配記憶體僅僅是把那個指標向空閒空間那邊挪動一段與物件大小相等的距離。在使用Serial、ParNew收集器時等帶有Compact過程時,系統分配演算法是指標碰撞。
  • 空閒列表:Java堆中記憶體不是規整的,已使用的記憶體和空閒的記憶體相互交錯,VM需維護一個列表,記錄上哪些記憶體是可用的,在分配時從列表中找到一塊足夠大的空間劃分給物件例項,並更新列表上的記錄。使用CMS收集器時,就是採用的空閒裡列表,CMS是基於Mark-Sweep演算法(標記-清除)的收集器。

併發安全問題

  Java物件建立在程式中是非常常見的,所以在VM中物件建立是非常頻繁,容易出現多執行緒併發安全問題:如程式中建立物件A和物件B,底層VM給A物件分配記憶體,指標沒來及修改,物件B同時使用原來的指標分配記憶體。
  解決方案有兩種:同步處理和本地執行緒分配緩衝

  • 同步處理:分配記憶體空間的動作進行同步處理(CAS操作),VM採用CAS配上失敗重試的方式保證更新操作的原子性
  • 本地執行緒分配緩衝:Thread Local Allocation Buffer, TLAB,把記憶體分配的動作按照執行緒劃分不同的空間之中進行,即每個執行緒在Java堆中預先分配一小塊記憶體,即為TLAB,哪個執行緒要分配記憶體,就在哪個執行緒的TLAB上分配,只有用完後並分配新的TLAB,才需要同步鎖定。通過-XX:+/-UseTLAB引數設定是否需要使用TLAB。

物件的記憶體佈局

概述

  Java物件在記憶體儲存的佈局分為3塊:物件頭、例項資料和對齊填充

物件頭

  物件頭(Header)分為兩部分:用於儲存物件自身的執行時資料和型別指標

執行時資料

  Mark Word,用於儲存物件自身的執行時資料包括:雜湊碼、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等。

儲存內容 標誌位 狀態
物件雜湊碼、GC分代年齡 01 未鎖定
指向鎖記錄的指標 00 輕量級鎖定
指向重量級的指標 10 膨脹(重量級鎖定)
空,不需要記錄資訊 11 GC標誌
偏向執行緒ID、偏向時間戳、物件分代年齡 01 可偏向

  Mark Word是一個非固定的資料結構,在極小的空間記憶體儲儘量多的資料,會根據物件的狀態複用自己的儲存空間,如在32位HotSpot VM中,若物件處於未鎖定狀態,Mark Word的32bit空間中25bit用於儲存物件雜湊碼,4bit用於儲存物件分代年齡,2bit用於儲存鎖標誌位,1bit固定為0,即32(儲存空間)=25(雜湊碼)+4(分代年齡)+2(鎖標誌位)+1(固定0)

型別指標

  即物件指向它的類元資料的指標,虛擬機器通過這個指標來確定物件是哪個類的例項,但是並非查詢物件的元資料就一定要通過物件本身,也只是適用於普通物件,普通Java物件可以通過元資料資訊可以確定Java物件的大小。不適用的Java物件,如Java陣列物件的物件頭中必須有一塊能保持記錄陣列長度的資料,因為從陣列元資料中無法確定陣列的大小。

例項資料

  例項資料(Instance Data)是物件真正儲存的有效資訊,也是程式程式碼中定義的各種型別的欄位內容。這部分儲存順序會受到VM分配策略引數欄位在Java原始碼中定義順序的影響。
VM預設分配策略
  HotSpot預設分配策略為longs/doubles、ints、shorts/chars、bytes/nooleans、oops,相同寬度的欄位會被分配到一起,在父類中定義的變數會出現在子類之前。

對齊填充

  對齊填充(Padding)是非必要的,只是起著佔位符的作用。VM自動記憶體管理系統要求物件起始地址(物件大小)必須是8位元組的整數倍,物件頭都是8位元組的整數倍,而例項資料部分若沒有8位元組的整數倍,可以通過對齊填充進行補全。

物件的訪問方式

概述

  Java程式通過棧上的reference資料類操作堆上的具體物件(棧中的區域性變量表儲存了物件名的變數,堆中儲存了物件的具體地址)。主流的物件訪問定位方式有兩種:使用控制代碼和直接指標

使用控制代碼

  使用控制代碼訪問物件,Java堆中會劃分出一塊記憶體作為控制代碼池reference中儲存的就是物件的控制代碼地址,控制代碼中包含了物件例項資料與型別資料各自的具體地址資訊。

直接指標

  使用直接指標訪問,Java堆物件的佈局中就必須考慮如何放置訪問型別資料的相關資訊,而reference中儲存的直接就是物件地址。(Sun HotSport VM的使用方式)

訪問方式對比

  使用控制代碼訪問優勢是reference中儲存的是穩定的控制代碼地址,物件被移動時,只會改變控制代碼中例項資料指標,reference本身不會變;
  使用直接指標訪問優勢是速度快,節省一次指標定位時間開銷。(JVM預設使用)