簡述
今天繼續寫《深入理解java虛擬機》的對象創建的理解。這次和上次隔的時間有些長,是因為有些東西確實不好理解,就查閱各種資料,然后弄明白了才來做記錄。
(此文中所闡述的內容都是以HotSpot虛擬機為例的。)
對象的創建
java程序在運行過程中無時無刻都有對象被創建出來,那么創建對象是個怎么樣的過程呢?還是看看我自己的理解吧。
判斷是否已經執行類加載
當虛擬機遇到一條new指令時 ,首先去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已經被加載、解析和初始化過,如果沒有,那必須先執行相應的類加載過程。(類加載過程,以后我也會單獨的介紹)
內存分配
當已經執行過類加載過程后,會為新對象在Java堆中分配一個大小已經確定的內存,具體的內存分配規則有兩種:
- 指針碰撞(Bump the Pointer)
如果Java堆中的內存是絕對規整的,所有用過的內存放一邊,空閑的內存放到一邊,中間放着指針為分界點,分配內存就是把指針向空閑的一邊挪動一段與對象大小相等的距離。 - 空閑列表 (Free List )
如果Java堆中的內存並不是規整對的,已使用的內存和空間相互交錯,虛擬機會將可以用的內存維護到一個列表上,在分配內存時從這個列表中找到一塊足夠大的空間划給對象。然后更新列表記錄。
Java堆中的內存是否是規整的是根據虛擬機所采用的垃圾收集器是否帶有壓縮整理功能決定的。Serial、ParNew帶壓縮整理的分配內存用指針碰撞,CMS這種通常用空閑列表方式分配內存(垃圾收集器我也會單獨介紹的,看來對象創建涉及的地方很多呢。)
防止並發
在虛擬機上創建對象是非常頻繁的行為,所以要做到防止並發,有以下兩種方式可實現:
- 堆分配內存空間的動作進行同步處理,實際上JVM采用CAS(Cmpare And Set)配上失敗重試的方式保證更新操作的原子性;
- 把內存分配的動作按照線程划分在不同的空間之中進行,即為每個線程在java堆中預先分配一塊小內存,稱為本地線程分配緩沖區(Thread Local Allocation Buffer,TLAB)。分配內存時在線程的TLBA上分配,只有TLAB用完並分配新的TLAB時,才需要同步鎖定。JVM是否使用TLAB可以通過-XX:+UseTLAB參數來設定。
初始化對象內存空間
內存分配完成后,JVM將分配到的內存空間都初始化為零值(不包括對象頭)。
對象頭的設置
將對象的類、哈希碼、對象的GC分代年齡等信息設置到對象頭之中。
執行Java的init方法
設置完對象頭后,從JVM的角度來看一個對象已經完成了,但是從java程序的角度來看還沒有創建完成呢。此時就需要執行init方法,調用構造方法等過程,這樣一個真正可用的對象才算完全的產生出來。
對象的內存布局
創建完對象后,對象對分配給自己的內存是如何布局的呢?下面來介紹一下。
對象在堆內存中的布局可分為三部分:對象頭(Header),實例數據(Instance Data),對齊填充(Padding)。
對象頭:對象頭包含兩部分,第一部分存儲自身運行時數據,如哈希碼,GC分代年齡、鎖狀態標志、線程持有鎖、偏向線程ID、偏向時間戳等,官方稱為“Mark Word”。
第二部分是類型指針,即對象指向它的類元數據的指針,通過此指針來確定是哪個類的對象。
實例數據:存儲對象中的各類型的字段內容。無論是從父類繼承來的還是在子類中定義的。
對齊填充:並不是必然存在的,當對象實例數據部分沒有對齊時,進行對齊補全。
對象的訪問定位
Java程序需要通過棧上的reference數據來操作堆上的具體對象。reference數據只是一個指向對象的引用,具體的對象訪問根據不同虛擬機有不同的實現,主流的訪問方式有兩種:使用句柄和直接指針。
使用句柄:
如果通過句柄來訪問對象,Java堆中會划出一塊內存作為句柄池,reference中存儲句柄地址,而句柄中包含對象的實例數據與類型數據各自的地址。這樣就能訪問到對象了。
直接指針:
直接指針,就是指reference中直接存儲對象的地址。但是Java堆對象的布局中就必須考慮如何防止訪問類型數據相關信息。
這兩種對象訪問方式,各有優勢,但是HotSpot使用的是指針對象訪問,但是句柄訪問對象在整個軟件開發范圍中也是十分常見的。
參考
《深入理解Java虛擬機》