對象的實例化內存佈局與訪問定位

對象的實例化

創建對象的方式:

從創建對象的執行步驟來分析 對象的創建過程:

  1. 判斷對象對應的類是否加載、鏈接、初始化。虛擬機遇到一條new指令,首先去檢查這個指令的參數能付在Metaspace的常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已經被加載、解析和初始化(即判斷類元信息是否存在)。如果沒有,那麼在雙親委派模式下,使用當前類加載器以ClassLoader+包名+類名爲key進行查找對應的.class文件。如果沒有找到文件,則拋出ClassNotFoundException異常,如果找到,則進行類加載,並生成對應的Class類對象。
  2. 爲對象分配內存。首先計算對象佔用空間大小,接着在堆中劃分一塊內存給新對象,如果實例成員變量是引用變量,僅分配引用變量空間即可,即4個字節大小。如果內存是規整的,那麼虛擬機將採用指針碰撞法來爲對象分配內存。假設java堆中內存是絕對規整的,所有用過的內存放一邊,未使用過的放一邊,中間有一個指針作爲臨界點,如果新創建了一個對象則是把指針往未分配的內存挪動與對象內存大小相同距離,這個稱爲指針碰撞。如果垃圾收集器選擇的是Serial、ParNew這種基於壓縮算法的,虛擬機採用這種分配方式。一般使用帶有整理過程的收集器時使用指針碰撞;如果內存不規整,則使用空閒列表法(Free List)。事實上,Java堆的內存並不是完整的,已分配的內存和空閒內存相互交錯,JVM通過維護一個列表,記錄可用的內存塊信息,當分配操作發生時,從列表中找到一個足夠大的內存塊分配給對象實例,並更新列表上的記錄。使用的GC收集器:CMS,適用堆內存不規整的情況下。
  3. 處理併發安全問題。在創建對象的時候有一個很重要的問題,就是線程安全,因爲在實際開發過程中,創建對象是很頻繁的事情,作爲虛擬機來說,必須要保證線程是安全的,通常來講,虛擬機採用兩種方式來保證線程安全:一是採用CAS, CAS 是樂觀鎖的一種實現方式。所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突而去完成某項操作,如果因爲衝突失敗就重試,直到成功爲止。虛擬機採用 CAS 配上失敗重試的方式保證更新操作的原子性;二是爲每個線程預先分配一塊TLAB——通過-XX:+/-UseTLAB參數來設定(JDK8及之後默認開啓),爲每一個線程預先分配一塊內存,JVM在給線程中的對象分配內存時,首先在TLAB分配,當對象大於TLAB中的剩餘內存或TLAB的內存已用盡時,再採用上述的CAS進行內存分配。
  4. 初始化分配到的空間,所有屬性設置默認值,保證對象實例字段在不賦值時可以直接使用。(這裏要區別一下類加載過程的準備階段,參考我的另一篇博客:https://blog.csdn.net/Jhno99/article/details/106296449
  5. 設置對象的對象頭。將對象的所屬類(即類的元數據信息)、對象的HashCode和對象的GC信息、鎖信息等數據存儲在對象的對象頭中。這個過程的具體設置方式取決於JVM的實現。
  6. 執行init方法進行初始化。在Java程序的視角看來,初始化才正式開始。初始化成員變量,執行實例化代碼塊,調用類的構造方法,並把堆內對象的首地址賦值給引用變量。因此一般來說(由字節碼中是否跟隨有invokespecial指令所決定),new指令之後會接着就是執行方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算完全創建出來。

對象的內存佈局

 可以以圖得形式將前邊涉及到的知識點組合來看。

               

上述代碼編譯後的JVM詳細情況

 

對象的訪問定位

一、句柄訪問。虛擬機棧的局部變量表中記錄了對象引用,指向堆空間中對應的句柄,句柄位於Java堆空間的句柄池中,一個句柄包含兩個指針,分別是到堆空間的實例池的對應對象實例數據的指針和到方法區的對象類型數據的指針。

二、直接指針,虛擬機棧的局部變量表中記錄的對象引用直接指向了對象實例數據,而在對象實例數據中有一個到對象類型數據的指針,指向方法區中相應額對象類型數據。HotSpot採用的就是這種直接指針法。

三、兩者比較。兩種訪問定位方式各有優劣,一方面,很明顯直接指針法要比句柄訪問的效率高一些,另一方面,對於句柄訪問而言,reference中存儲穩定的句柄地址,對象被移動(垃圾收集時移動對象很普遍)時只會改變句柄中實例數據指針即可,reference本身不需要被修改,而對於直接指針而言,對象移動的話reference也要修改。

 

 

 

 

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