1、對象的創建
java是面向對象的語言,因此對象的創建無時無刻都存在。在語言層面,使用new關鍵字即可創建出一個對象。但是在虛擬機中,對象創建的創建過程則是比較復雜的。
首先,虛擬機運到new指令時,會去常量池檢查是否存在new指令中包含的參數,比如new People(),則虛擬機首先會去常量池中檢查是否有People這個類的符號引用,並且檢查這個類是否已經被加載了,如果沒有則會執行類加載過程。
在類加載檢查過后,接下來為對象分配內存當然是在java堆中分配,並且對象所需要分配的多大內存在類加載過程中就已經確定了。為對象分配內存的方式根據java堆是否規整分為兩個方法:1、指針碰撞(Bump the Pointer),2、空閑列表(Free List)。指針碰撞:如果java堆是規整的,即所有用過的內存放在一邊,沒有用過的內存放在另外一邊,並且有一個指針指向分界點,在需要為新生對象分配內存的時候,只需要移動指針畫出一塊內存分配和新生對象即可;空閑列表:當java堆不是規整的,意思就是使用的內存和空閑內存交錯在一起,這時候需要一張列表來記錄哪些內存可使用,在需要為新生對象分配內存的時候,在這個列表中尋找一塊大小合適的內存分配給它即可。而java堆是否規整和垃圾收集器是否帶有壓縮整理功能有關。
在為新生對象分配內存的時候,同時還需要考慮線程安全問題。因為在並發的情況下內存分配並不是線程安全的。有兩種方案解決這個線程安全問題,1、為分配內存空間的動作進行同步處理;2、為每個線程預先分配一小塊內存,稱為本地線程分配緩存(Thread Local Allocation Buffer, TLAB),哪個線程需要分配內存,就在哪個線程的TLAB上分配。
內存分配后,虛擬機需要將每個對象分配到的內存初始化為0值(不包括對象頭),這也就是為什么實例字段可以不用初始化,直接為0的原因。
接來下,虛擬機對對象進行必要的設置,例如這個對象屬於哪個類的實例,如何找到類的元數據信息。對象的哈希嗎、對象的GC年代等信息,這些信息都存放在對象頭之中。
執行完上面工作之后,所有的字段都為0,接着執行<init>指令,把對象按照程序員的指令進行初始化,這樣一個對象就完整的創建出來。
2、對象的內存布局
對象在內存的存儲布局中包括:對象頭、實例數據、對齊填充
對象頭(Header):包含兩部分信息。1、存儲對象自身的運行時數據,比如哈希碼、GC分代年齡等;2、類型指針:通過這個指針確定這個對象屬於哪個類。
實例數據(Instance Data):存儲代碼中定義的各種類型的字段內容。
對齊填充(Padding):這部分信息沒有任何意義,僅僅是為了使得對象占的內存大小為8字節的整數倍。
3、對象的訪問定位
創建對象是為了使用對象,java程序需要通過棧上的reference數據來操作棧上的具體對象。目前主流的訪問對象方式有使用句柄和直接指針兩種。1、使用句柄方式:會在java堆中創建一個句柄池,reference指向的這塊句柄池,句柄池中包括兩個指針,其中一個指針指向對象實例數據,另外一個指針指向對象的類型數據。2、使用指針的方式:reference存儲的直接就是對象的地址。
兩種方式各有各的特點,如果使用句柄方式的話,最大的好處是reference存放的是穩定的句柄地址,在對象移動時只會改變句柄中的實例數據指針,而reference本身不需要修改。使用指針的方式優勢則是速度快,並且省去了一次指針定位的開銷。