關注微信公眾號:CodingTechWork,一起學習進步。
引言
Java程序員都知道如何創建對象,不就是一個Person person = new Person()
的語句就解決了么?然而,我們只知道new,卻對於底層如何實現對象的創建、如何存儲到內存中去、又如何被訪問的知之甚少。
對象的創建
流程圖
創建流程
- Java程序new一個對象。
- 虛擬機遇到一條
new指令
時,首先檢查這個指令的參數是否能在常量池中定位到一個類的符號引用
,且檢查該符號引用代表的類是否已被加載、解析和初始化過
。若沒有,需先進行相應的類加載過程。 - 在類加載檢查通過后,虛擬機將為新生對象
分配內存
。(對象在內存中所需要的大小在類加載完成后就確定了) - 內存分配完之后,虛擬機需要將分配到的內存空間
初始化為零值
(不包括對象頭)。保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,可以訪問對應的零值。(對應准備階段) - 虛擬機對對象進行必要的設置(
對象頭的設置
)。如這個對象是哪個類的實例、如何找到類的元數據信息、對象哈希碼、對象的GC分代年齡等信息。 - 以上虛擬機中新對象產生,對應到Java程序還需要繼續執行
<init>
方法,將對象在程序中進行初始化。
內存空間分配方式
為對象分配空間就是從Java堆
中划分出一塊確定大小的內存給新生對象,考慮符合划分可用空間的兩種方式:“指針碰撞”和“空閑列表”
- 指針碰撞:若Java堆中內存是絕對規整的,所有用過的內存都放在一邊,空閑的內存放在另一邊,中間放着一個
指針作為分界點的指示器
,所分配內存僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離
。在使用Serial、ParNew收集器時
等帶有Compact過程
時,系統分配算法是指針碰撞。 - 空閑列表:Java堆中內存不是規整的,已使用的內存和空閑的內存相互交錯,VM需維護一個列表,記錄上哪些內存是可用的,在分配時從列表中找到一塊足夠大的空間划分給對象實例,並更新列表上的記錄。使用
CMS收集器
時,就是采用的空閑里列表,CMS是基於Mark-Sweep算法(標記-清除)
的收集器。
並發安全問題
Java對象創建在程序中是非常常見的,所以在VM中對象創建是非常頻繁,容易出現多線程並發安全問題
:如程序中創建對象A和對象B,底層VM給A對象分配內存,指針沒來及修改,對象B同時使用原來的指針分配內存。
解決方案有兩種:同步處理和本地線程分配緩沖
- 同步處理:分配內存空間的動作進行同步處理(CAS操作),VM采用
CAS配上失敗重試
的方式保證更新操作的原子性
; - 本地線程分配緩沖:Thread Local Allocation Buffer, TLAB,把內存分配的動作
按照線程划分
在不同的空間
之中進行,即每個線程在Java堆中預先分配一小塊內存
,即為TLAB,哪個線程要分配內存,就在哪個線程的TLAB上分配,只有用完后並分配新的TLAB,才需要同步鎖定。通過-XX:+/-UseTLAB
參數設定是否需要使用TLAB。
對象的內存布局
概述
Java對象在內存存儲的布局分為3塊:對象頭、實例數據和對齊填充
。
對象頭
對象頭(Header)分為兩部分:用於存儲對象自身的運行時數據和類型指針
。
運行時數據
Mark Word
,用於存儲對象自身的運行時數據包括:哈希碼、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等。
存儲內容 | 標志位 | 狀態 |
---|---|---|
對象哈希碼、GC分代年齡 | 01 | 未鎖定 |
指向鎖記錄的指針 | 00 | 輕量級鎖定 |
指向重量級的指針 | 10 | 膨脹(重量級鎖定) |
空,不需要記錄信息 | 11 | GC標志 |
偏向線程ID、偏向時間戳、對象分代年齡 | 01 | 可偏向 |
Mark Word是一個非固定
的數據結構,在極小的空間內存儲盡量多的數據,會根據對象的狀態復用
自己的存儲空間,如在32位HotSpot VM中,若對象處於未鎖定狀態,Mark Word的32bit空間中25bit用於存儲對象哈希碼,4bit用於存儲對象分代年齡,2bit用於存儲鎖標志位,1bit固定為0,即32(存儲空間)=25(哈希碼)+4(分代年齡)+2(鎖標志位)+1(固定0)
類型指針
即對象指向它的類元數據的指針
,虛擬機通過這個指針來確定對象是哪個類的實例,但是並非查找對象的元數據就一定要通過對象本身,也只是適用於普通對象,普通Java對象可以通過元數據信息
可以確定Java對象的大小。不適用的Java對象,如Java數組對象
的對象頭中必須有一塊能保持記錄數組長度的數據,因為從數組元數據中無法確定數組的大小。
實例數據
實例數據(Instance Data)是對象真正存儲的有效信息
,也是程序代碼中定義的各種類型的字段內容
。這部分存儲順序會受到VM分配策略參數
和字段在Java源碼中定義順序
的影響。
VM默認分配策略
HotSpot默認分配策略為longs/doubles、ints、shorts/chars、bytes/nooleans、oops,相同寬度的字段會被分配到一起,在父類中定義的變量會出現在子類之前。
對齊填充
對齊填充(Padding)是非必要
的,只是起着占位符的作用
。VM自動內存管理系統要求對象起始地址(對象大小)必須是8字節的整數倍
,對象頭都是8字節的整數倍,而實例數據部分若沒有8字節的整數倍,可以通過對齊填充進行補全。
對象的訪問方式
概述
Java程序通過棧上的reference數據類操作堆上的具體對象(棧中的局部變量表存儲了對象名的變量,堆中存儲了對象的具體地址)。主流的對象訪問定位方式有兩種:使用句柄和直接指針
。
使用句柄
使用句柄訪問對象,Java堆中會划分出一塊內存作為句柄池
,reference中存儲
的就是對象的句柄地址
,句柄中包含了對象實例數據與類型數據各自的具體地址信息。
直接指針
使用直接指針訪問,Java堆對象的布局中就必須考慮如何放置訪問類型數據的相關信息,而reference中存儲
的直接就是對象地址
。(Sun HotSport VM的使用方式)
訪問方式對比
使用句柄訪問優勢是reference中存儲的是穩定的句柄地址,對象被移動時,只會改變句柄中實例數據指針,reference本身不會變;
使用直接指針訪問優勢是速度快,節省一次指針定位時間開銷。(JVM默認使用)
參考
《深入理解Java虛擬機》