《深刻理解Java虛擬機》(二):對象的建立

《深刻理解Java虛擬機》(二):對象的建立

==============程序員

讀書筆記系列

==============編程

圖1. 對象的建立過程安全

圖2. 對象的內存佈局併發

圖3. 對象的訪問定位編程語言

系列上一篇介紹了運行時數據區以後,咱們大體知道了虛擬機內存的概況。本篇呢,就大概介紹一下關於內存的使用——對象的建立。想要節省時間的同窗,只需瞭解上面的圖片便可。內容如有紕漏,還望不吝指出。oop

1. 對象的建立過程

衆所周知,Java 是一門面向對象的編程語言,沒有對象怎麼辦?new 一個就好啦!在語言層面上,建立對象(例如克隆、反序列化)一般僅僅是一個 new 關鍵字而已,而在虛擬機中,對象的建立又是怎樣的過程呢?佈局

虛擬機遇到一條 new 指令時,首先進行類加載檢查。檢查這個指令的參數可否在常量池中定位到一個類的符號引用,而且檢查這個符號引用表明的類是否已被加載、解析和初始化過,若是沒有,那必須先執行相應的類加載過程(以後的系列文章會介紹到)。線程

接下來虛擬機將爲新生的對象分配內存。對象所需內存的大小在類加載完成後即可徹底肯定,爲對象分配空間的任務等同於把一塊肯定大小的內存從 Java 堆中劃分出來。假設 Java 堆中內存是絕對規整的,全部用過的內存的都放在一邊,空閒的內存都放在一邊,中間放着一個指針做爲分界點的指示器,那所分配的內存就僅僅是把那個指針向空閒空間那邊移動一段與對象大小相等的距離,這種分配方式稱爲 「指針碰撞」(Bump the Pointer)。若是 Java 堆是不規整的,已使用的內存和空閒的內存相互交錯,虛擬機就必須維護一個列表,記錄哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄,這種分配方式稱爲 「空閒列表」(Free List)。另一個須要考慮的問題是,對象建立在虛擬機中是很是頻繁的行爲,即便是僅僅修改指針所指向的位置,在併發狀況下也不是線程安全的,可能出現正在給對象 A 分配內存,指針還沒來得及修改,對象 B 又同時使用了原先的指針來分配內存的狀況。解決方案有兩種:一種是對分配內存空間的動做進行同步處理——實際上虛擬機採用 CAS 配上失敗重試的方法保證更新操做的原子性;另外一種是把內存分配的動做按照線程劃分在不一樣空間之中進行,即每一個線程在 Java 堆中預先分配一小塊內存,稱爲本地線程分配緩衝(Thread Local Allocation Buffer,TLAB)。哪一個線程須要分配內存,就在哪一個線程的 TLAB 上分配,只有 TLAB 用完並分配新的 TLAB 時,才須要同步鎖定。3d

內存分配完成後,虛擬機須要將分配到的內存空間都初始化爲零值(不包括對象頭)。若是使用 TLAB ,這一工做過程也能夠提早至 TLAB 分配時進行。這一步操做保證了對象的實例字段在 Java 代碼中能夠不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。接下來,虛擬機要對對象進行必要的設置,例如對象是哪一個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的 GC 分代年齡等信息。指針

在上面的工做都完成以後,從虛擬機的視角來看,一個新的對象已經產生了,但從 Java 程序的視角來看,對象建立纔剛剛開始——<init> 方法尚未執行,全部的字段都還爲零。因此,通常來講,執行 new 指令以後會接着 執行 <init> 方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算徹底產生出來。

2. 對象的內存佈局

注:如下虛擬機均是指 HotSpot 虛擬機。

在虛擬機中,對象在內存中存儲的佈局能夠分爲 3 塊區域:對象頭(Header)實例數據(Instance Data)對齊填充(Padding)

虛擬機的對象頭包括兩部分信息:第一部分用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC 分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程 ID、偏向時間戳等,這部分數據的長度在 32 位和 64 位虛擬機(未開啓壓縮指針)中分別爲 32bit64bit,官方稱它爲 「Mark Word」;另外一部分是類型指針,即對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例。

實例數據部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各類類型字段。不管是從父類中繼承的,仍是在子類中定義的,都須要記錄下來。其存儲順序會收到虛擬機分配策略參數(FieldAllocationStyle)和字段在 Java 源碼中定義順序的影響。HotSpot 虛擬機默認的分配策略爲 longs/doublesintsshorts/charsbytes/booleansoops(Ordinary Object Pointers),從分配策略中能夠看出,相同寬度的字段老是被分配到一塊兒。在知足這個前提條件的狀況下,在父類中定義的變量會出如今子類以前。若是 CompactFields 參數值爲 true(默認爲 true),那麼子類中較窄的變量也可能會插入到父類變量的空隙之中。

第三部分對齊填充並非必然存在的,也沒有特別的含義,它僅僅起着佔位符的做用。因爲虛擬機的自動內存管理系統要求對象起始地址必須 8 字節的整數倍,換句話說,就是對象的大小必須是 8 字節的整數倍。而對象頭部分正好是 8 字節的倍數,所以,當對象實例數據部分沒有對齊時,就須要經過對齊填充來補全。

3. 對象的訪問定位

創建對象是爲了使用對象,Java 程序須要經過棧上的 reference 數據來操做堆上的具體對象。因爲 reference 類型在 Java 虛擬機規範中只規定了一個指向對象的引用,因此對象的訪問方式也是取決於虛擬機實現而定的,目前主流的訪問方式有 使用句柄直接指針 兩種。

  • 使用句柄訪問的話,那麼 Java 堆中將會劃分出一塊內存來做爲句柄池,refenerce 中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址。

  • 使用直接指針訪問,那麼 Java 堆對象的佈局中就必須考慮如何防止訪問類型數據的相關信息,而 reference 中存儲的直接就是對象地址。

這兩種對象訪問方式各有優點,使用句柄來訪問的最大好處是 reference 中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是很是廣泛的行爲)是隻會改變句柄中的實例數據指針,而 reference 自己不須要修改。

使用直接指針訪問方式的最大好處就是訪問速度快,它節省了一次指針定位的時間開銷,因爲對象的訪問在 Java 中很是頻繁,所以這類開銷聚沙成塔後也是一項很是可觀的執行成本。

總結

以上即是本篇筆記的內容了,咱們這裏瞭解到了對象的建立流程對象的內存佈局,以及對象的訪問定位

相關文章
相關標籤/搜索