這里以HotSpot為例,且所說的對象指普通的Java對象,不包括數組和Class對象等。
1、對象創建的過程
1、類加載、解析、初始化:虛擬機遇到new時先檢查此指令的參數是否能在常量池中找到類的符號引用,並檢查符號引用代表的類是否被加載、解析、初始化,若沒有則先進行類加載。
2、對象內存分配:類加載檢查通過后,虛擬機為新生對象分配內存,對象所需內存大小在類加載完成后便可完全確定。分配內存的任務等同於從堆中分出一塊確定大小的內存。(具體分配策略見:JVM內存分配策略-MarchOn)
根據內存是否規整(即用的放一邊,空閑的放另一邊,是否如此與所使用的垃圾收集器是否帶有壓縮整理Compact功能有關),分配方式分為指針碰撞(Serial、ParNew等收集器)和空閑列表(CMS收集器等)兩種
並發控制:可能多個對象同時從堆中分配內存因此需要同步,兩種解決方案:虛擬機用CAS配上失敗重試保證原子操作;把內存分配動作按線程划分在不同空間中進行,即每個線程預先分配一塊線程本地緩沖區TLAB,各線程在各自TLAB為各自對象分配內存。
3、對象的初始化:對象頭和對象實例數據的初始化
2、對象的內存布局
布局:
- 對象頭:標記字(32位虛擬機4B,64位虛擬機8B) + 類型指針(32位虛擬機4B,64位虛擬機8B)+ [數組長(對於數組對象才需要此部分信息)]
- 實例數據
- 對齊填充:對於64位虛擬機來說,對象大小必須是8B的整數倍,不夠的話需要占位填充
- 對象頭用於存儲對象的元數據信息:
- Mark Word 部分數據的長度在32位和64位虛擬機(未開啟壓縮指針)中分別為32bit和64bit,存儲對象自身的運行時數據如哈希值等。Mark Word一般被設計為非固定的數據結構,以便存儲更多的數據信息和復用自己的存儲空間。
- 類型指針 指向它的類元數據的指針,用於判斷對象屬於哪個類的實例。
- 實例數據存儲的是真正有效數據,如各種字段內容,各字段的分配策略為longs/doubles、ints、shorts/chars、bytes/boolean、oops(ordinary object pointers),相同寬度的字段總是被分配到一起,便於之后取數據。父類定義的變量會出現在子類定義的變量的前面。
- 對齊填充部分僅僅起到占位符的作用,並非必須。
示例(以HashMap<Long,Long>為例):
其只有Key和Value是有效數據,共2*8B=16B,包裝成Long對象后分別具有了8B標記字和8B的類型指針,共24B*2=48B;兩個對象組成Map.Entry后多了16B對象頭、一個8B的next字段、4B的int類型的hash字段,還必須添加4B的空白填充。共32B;最后還有對HashMap中對此Entry的8B的引用。所以空間利用率為 16B / (48B+32B+8B) ≈ 18%
32位虛擬機 VS 64位虛擬機:
由於指針膨脹和各種數據類型對齊補白的原因,運行於64位系統上的Java應用需要消耗更多的內存(通常比32位的增加10%~30%的內存開銷) ;此外,64位虛擬機的運行速度比32位的大約有15%左右的性能差距。
不過,64位虛擬機也有它的優勢:首先能管理更多的內存,32位最多4GB,實際上還受OS允許進程最大內存的限制(Windows下2GB);其次,隨着硬件技術的發展,計算機終究會完全過渡到64位,虛擬機也將過渡到64位。
3、對象的訪問定位
對象的訪問定位也取決於具體的虛擬機實現。當我們在堆上創建一個對象實例后,就要通過虛擬機棧中的reference類型數據來操作堆上的對象。現在主流的訪問方式有兩種(HotSpot虛擬機采用的是第二種):
- 使用句柄訪問對象。即reference中存儲的是對象句柄的地址,而句柄中包含了對象實例數據與類型數據的具體地址信息,相當於二級指針。
- 直接指針訪問對象。即reference中存儲的就是對象地址,相當於一級指針。
兩種方式有各自的優缺點。當垃圾回收移動對象時,對於方式一而言,reference中存儲的地址是穩定的地址,不需要修改,僅需要修改對象句柄的地址;而對於方式二,則需要修改reference中存儲的地址。從訪問效率上看,方式二優於方式一,因為方式二只進行了一次指針定位,節省了時間開銷,而這也是HotSpot采用的實現方式。下圖是句柄訪問與指針訪問的示意圖。
思考:
一個對象的實例變量存哪里?堆還是棧?
答:應該是堆,實例變量用在對象內的偏移量表示並存儲了。對么?