Java虛擬機器:Java物件建立和物件訪問
1.物件建立
Java是一門面向物件的語言,Java程式執行過程中無時無刻都有物件被創建出來。在語言層面上,建立物件(克隆、反序列化)就是一個new關鍵字而已,但是虛擬機器層面上卻不是如此。看一下在虛擬機器層面上建立物件的步驟:
物件的建立過程
圖一:物件建立過程
1、類載入檢查。
當JVM檢測到有一條new指令時,首先先檢查該指令的引數是否在常量池中定位到一個類的符號引用,並檢查這個符號引用所代表的類是否已被載入、解析和初始化過。如果存在的話,JVM將直接使用已有的資訊對該類進行操作。
如果沒有,則執行相應的類載入過程。
2、虛擬機器為新生物件分配內容(位於堆中)。
類載入檢查通過後,虛擬機器為新生物件分配記憶體,
不同的JVM垃圾收集器在分配記憶體時的表現也不相同,具體表現為兩種:
(1)如果垃圾收集器選擇的是Serial、ParNew這種基於壓縮整理演算法的,那麼記憶體是規整的,虛擬機器將採用的是指標碰撞法來為物件分配記憶體。意思是所有用過的記憶體在一邊,空閒的記憶體在另外一邊,中間放著一個指標作為分界點的指示器,分配記憶體就僅僅是把指標向空閒那邊挪動一段與物件大小相等的距離罷了。
(2)如果垃圾收集器選擇的是CMD這種基於標記-清除演算法的,那麼記憶體不是規整的,已使用的記憶體和未使用的記憶體相互交錯,虛擬機器將採用的是空閒列表法來為物件分配記憶體。意思是虛擬機器維護了一個列表,記錄上哪些記憶體塊是可用的以及記憶體塊的位置和大小,再分配的時候從列表中找到一塊足夠大的空間劃分給物件例項,並更新列表上的內容。
另外一個問題是new物件時的執行緒安全性,也就是記憶體分配時的同步問題。因為可能出現虛擬機器正在給物件A分配記憶體,指標還沒有來得及修改,物件B又同時使用了原來的指標來分配記憶體的情況。這種情況下虛擬機器會通過兩種方式進行同步:
a、CAS和失敗重試機制:對分配記憶體空間的動作進行同步處理,虛擬機器採用CAS配上失敗重試的方式保證更新操作的原子性。CAS簡單解釋就是:比較並交換,通過3\運算元,記憶體值V,舊的預期值A,要修改的新值B。當且僅當預期值A和記憶體值V相同時,將記憶體值V修改為B,否則什麼都不做。
b、TLAB方式:把記憶體的分配動作按照執行緒劃分在不同的空間之中進行,即每個執行緒在Java堆中先預留一塊本地執行緒分配緩衝(TLAB)。哪個執行緒分配記憶體時,就在哪個執行緒的TLAB分配,只有當TLAB用完並分配新的TLAB時,才需要同步鎖定。
3、記憶體分配結束。
記憶體分配結束,虛擬機器將分配到的記憶體空間都初始化為零值(不包括物件頭)。這一步保證了物件的例項欄位在Java程式碼中可以不用賦初始值就可以直接使用,程式能訪問到這些欄位的資料型別所對應的零值。
4、對物件進行必要的設定。
對物件進行必要的設定,例如這個物件是哪個類的例項、如何才能找到類的元資料資訊、物件的雜湊碼、物件的GC分代年齡等資訊。這些資訊存放在物件的物件頭之中。
5、初始化物件(執行<init>方法)。
當完成上述操作後,物件的記憶體便分配成功了,但是所有的欄位都還是零。
此時應該執行<init>方法,把物件按照程式設計師的意願進行初始化,從而產生一個真正可用的物件。
下面我們再將上面的過程,重新畫一張圖,總結一下:
2.物件的記憶體佈局
物件的記憶體佈局分為三個區域:
a、物件頭,b、例項資料,c、對齊填充。
- 物件頭:非固定的資料結構。一來是用來儲存物件自身的執行時資料,如雜湊碼、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等。二來是型別指標,即物件指向它的類元資料的指標、JVM通過這個指標來確定這個物件是哪個類的例項。如果物件是一個Java陣列,則在物件頭中還需要有一塊記錄陣列長度資料。
- 例項資料:儲存物件真正有效的資訊,也就是程式程式碼中所定義的各種型別的欄位內容。不論是從父類繼承下來的,還是在子類中定義的。這部分的儲存順序會受到Java原始碼中定義順序的影響。
- 對齊填充:不一定必須存在。啟到佔位符的作用。因為JVM的自動記憶體管理系統要求物件的起始地址必須是8位元組的整數倍,即物件的大小必須是8位元組的整數倍。故當物件例項資料部分沒有對齊時,就需要通過對齊填充來補全。
3.物件的訪問方式
建立物件是為了使用物件,Java程式需要通過棧上的reference(引用)資料來操作堆上的具體物件。物件訪問會涉及到Java棧、Java堆、方法區這三個記憶體區域。
如下面這句程式碼:
Object obj = new Object();
上面物件例項化的其實有兩部分內容,一部分是類資料(比如代表類的Class物件)、一部分是例項資料
假如這句程式碼出現在方法體中,"Object obj" 這部分會作為引用型別(reference)的資料儲存在Java棧的本地變量表中。而"new Object()"這部分例項化物件將會反映到Java堆中,形成一塊儲存Object例項化物件的所有例項資料值的結構化記憶體,根據具體資料型別以及虛擬機器實現的物件記憶體佈局的不同,這塊記憶體的長度是不固定的。另外,在Java堆中還必須包含能查到此物件型別資料(如物件型別、父類、實現的介面、方法等)的地址資訊,這些資料型別則儲存在方法區中。
reference型別在java虛擬機器規範裡面只規定了一個指向物件的引用地址,並沒有定義這個引用應該通過哪種方式去定位,訪問到java堆中的物件位置,因此不同的虛擬機器實現的訪問方式可能不同,
主流的方式有兩種:使用控制代碼和直接指標:
1、使用控制代碼訪問,Java堆中將會劃分出一塊記憶體來作為控制代碼池,obj(reference引用)中儲存的是物件的控制代碼地址,而控制代碼中包含了類資料的地址和物件例項資料的地址。
2、直接指標訪問,Java堆中也就是物件中儲存所有的例項資料和類資料的地址,此時obj(reference引用)存放的是物件地址。
兩種訪問方式的對比:
- 使用控制代碼時,當改變控制代碼中的例項資料指標時,reference本身不需要被修改。
- 使用直接指標訪問最大的好處在於速度較快,因為其節省了一次指標定位的時間開銷。
目前使用直接指標訪問的方式比較常用,HotSpot虛擬機器採用的是後者,因為物件的訪問在Java程式執行過程中是比較頻繁的,積少成多也會造成太多的時間開銷。不過前者的物件訪問方式也是十分常見的。
下面拷貝兩張圖,明白一下物件的兩種訪問方式: