JVM02-JVM的對象創建以及訪問方式

前言

上一篇我們介紹了JVM的內存區域佈局,並且重點介紹了堆和棧的概念。,今天我們接着來學習JVM的對象創建過程已經對象的訪問方式。

對象創建

在這裏插入圖片描述
對象的創建共有如上五個步驟:

1.類加載檢查

虛擬機遇到一條new指令時,首先將去檢查這個指令是否在常量池中定位到這個類的符號引用,並且檢查這個符號引用代表的類是否已經被加載過、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。JVM中類加載是通過雙親委派模型來完成的雙親委派模型加載類

2.分配內存

類加載檢查通過後,接下來虛擬機將爲新生成對象分配內存。對象所需的內存大小在類加載完成後便可確定,爲對象分配空間的任務等同於把一塊確定大小的內存從Java堆中劃分出來。分配方式有"指針碰撞"和"空閒列表"兩種,選擇那種分配方式由Java堆是否規整決定。而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定。

分配內存的方式

  1. 指針碰撞
    通過"指針碰撞"分配內存的方式適用場合是
    堆內存規整(即沒有內存碎片) 的情況下。它的實現原理是:用過的內存全部整合到一邊,沒有用到的內存放在另一邊,只需要向着沒用過的內存方向將該指針移動對象內存大小位置即可。使用這種方式分配內存的垃圾收集器有:Serial收集器和ParNew收集器。
  2. 空閒列表
    通過"空閒列表"分配內存的方式適用場景是 堆內存不規整的情況。它的實現原理是:虛擬機會維護一個列表,該列表中會記錄哪些內存塊是可用的,在分配的時候,找一塊足夠大的內存塊來劃分對象實例,最後更新列表記錄 。使用這種方式分配內存的垃圾收集器有:CMS收集器。

內存分配的併發問題

在實際項目中,創建對象是很頻繁的事情,虛擬機採用兩種方式來保證線程安全:

  1. CAS+失敗重試: CAS是樂觀鎖一種實現方式所謂樂觀鎖就是,每次不加鎖而是假設沒有衝突去完成某項操作,如果失敗就進行重試操作,直到重試成功。虛擬機採用CAS加上失敗重試的方式保證更新操作的原子性
  2. TLAB: 爲每一個線程預先在Eden區分配一塊內存,JVM在給線程中的對象分配內存時,首先在TLAB分配,當對象大於TLAB中剩餘內存或TLAB的內存已用盡時,再採用上述的CAS進行內存分配。

3.初始化零值

內存分配完成之後,虛擬機需要將分配到的內存空間都初始化爲零值(不包括對象頭),這一步操作保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用程序能訪問到這些字段的數據類型所對應的零值。

4.設置對象頭:

初始化零值完成之後,虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象頭中。另外,根據虛擬機當前運行狀態的不同,是否啓用偏向鎖等,對象頭會有不同的設置方式。

5. 執行init方法;

在上面工作都完成之後,從虛擬機的視角來看,一個新的對象已經產生了,但是從Java程序的視角來看,對象創建纔剛開始,init方法還沒有執行,所有的字段都還爲零,所有一般來說,執行new指令之後會接着執行init方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算完成產生出來。

對象內存佈局

對象在內存中的佈局可以分3塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。

對象頭

虛擬機對象的對象頭部分包括兩類信息。
第一類是用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等長度在32位和64位的虛擬機中分別爲32bit和64個bit,官方稱爲"Mark Word"。
例如在32位的HotSpot虛擬機中,如對象未被同步鎖鎖定的狀態下,Mark Word的32個比特存儲空間中的25個比特用於存儲對象哈希碼,4個比特用於存儲對象分代年齡,2個比特用於存儲鎖標誌位,1個比特固定爲0狀態(輕量級鎖定、重量級鎖定、GC標記、可偏向)下對象的存儲內容如下所示:
在這裏插入圖片描述
對象頭的另外一部分是類型指針,即對象指向他的類型元數據的指針,Java虛擬機通過這個指針來確定該對象是哪個類的實例。並不是所有的虛擬機實現都必須在對象數據上保留類型指針。還一句話說,查找對象的信息並不一定要經過對象本身,比如,如果對象是一個Java數組,那在對象頭中還必須有一塊用於記錄數組長度的數據,因爲虛擬機可以通過普通Java對象的元數據信息可以確定Java對象的大小,但是如果數組的長度是確定的,將無法通過元數據中的信息推斷數組的大小。

實例數據

實例數據部分就是對象真正存儲的有效信息,我們在程序代碼裏所定義的各種類型的字段內容,無論是從父類繼承下來的,還是在子類中定義的字段都必須記錄下來。這部分的存儲順序會受到虛擬機分配策略參數(-XX: FieldsAllocationStyle參數)和字段在Java源碼中定義的順序的影響。虛擬機默認的分配順序爲 longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs) ,從以上默認的分配策略中可以看到,相同寬度的字段總是被分配到一起存放,在滿足這個前提條件的情況下,在父類中定義的變量會出現在子類之前。如果虛擬機的+XX: CompactFields參數值爲true(默認就爲true),那子類之中叫窄的變量也允許插入父類變量的空隙之中,以節省一點點空間。

對齊填充

對齊填充這部分不是必然存在的,也沒有特別的含義,它僅僅起着佔位符的作用。

對象訪問方式

我們的Java程序會通過棧上的reference數據來操作堆上的具體對象。對象的訪問方式也是由虛擬機實現的,主流的訪問方式主要有使用句柄和直接指針兩種。

使用句柄訪問

使用句柄訪問的話,Java堆中將會劃分出一塊內存作爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自具體的地址信息。其結構如下圖所示:
在這裏插入圖片描述

使用直接指針訪問

使用直接指針訪問的話,Java堆中對象的內存佈局就必須考慮如何訪問類型數據的相關信息,reference中存儲的直接就是對象地址,如果只是訪問對象本身的話,就不需要多一次間接的訪問的開銷。HosSpot而言,主要使用的是這種訪問方式。其結構如下圖所示:
在這裏插入圖片描述

使用直接指針來訪問最大的好處就是速度更快,它節省了一次指針定位的時間開銷,由於對象訪問在Java中非常頻繁,因此這類開銷積少成多也是一項極爲可觀的執行成本。

下面舉例說明:

Student stu =new Student("張三""18");

當我們拿到stu對象時,直接調用stu.getName();時,其實就完成了對對象的訪問,這裏stu只是一個變量,變量裏存儲的是指向對象的指針,當我們調用stu.getName()時,虛擬機會根據指針找到堆裏面的對象然後拿到實例數據name,需要注意的是,當我們調用stu.getClass()時,虛擬機會首先根據stu指針定位到堆裏面的對象,然後根據對象頭裏面存儲的指向Class類元信息的指針再次到方法區拿到Class對象,進行了兩次指針尋找。具體講解圖如下:
在這裏插入圖片描述

總結

本文首先介紹了JVM中對象的創建過程,接着就是介紹對象的內存佈局,最後就是說到了對象的訪問方式,其中對象的創建過程比較重要的一塊內容就是分配內存主要內容來自於《深入理解Java虛擬機_JVM高級特性與最佳實踐_第3版》

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