對象的創(chuàng)建過程
1、類加載
當(dāng)虛擬機(jī)遇到一個(gè) new 指令的時(shí)候,會先去檢測這個(gè)指令的參數(shù)是否能定位到這個(gè)類的符號引用,并檢查這個(gè)類是否被加載、解析、初始化過(在 JVM 的方法區(qū)中檢查)。如果沒有,則執(zhí)行類加載(類加載機(jī)制)
2、內(nèi)存分配
在類加載通過之后,虛擬機(jī)將為新生對象分配內(nèi)存,對象所需內(nèi)存的大小在類加載完成后便可完全確定,相當(dāng)于從 Java 堆中抽取一塊內(nèi)存出來;而根據(jù)內(nèi)存的是否絕對規(guī)整,分為指針碰撞和空閑列表兩種分配方式:
指針碰撞:假設(shè) Java 堆中的內(nèi)存是絕對規(guī)整的,分為空閑和非空閑兩種,中間用一個(gè)指針當(dāng)做劃分界限的指示器;當(dāng)一個(gè)新對象需要分配對象時(shí),相當(dāng)于把指針向空閑區(qū)域移動一段與對象大小相等的距離。
空閑列表:假設(shè) Java 堆的內(nèi)存不是絕對規(guī)整的,空閑和非空閑是相互交錯(cuò)的,那就需要一個(gè) OopMap 列表,用來記錄哪些內(nèi)存塊是可以用的,在對象分配內(nèi)存時(shí),劃分一塊大小相等的區(qū)域給對象,并更新這個(gè)列表
從上面的解釋看,用哪種分配方式,是通過 Java 堆的內(nèi)存塊是否絕對規(guī)整決定的。
堆內(nèi)存是否規(guī)整,主要是看 GC 回收了內(nèi)存之后是否包含壓縮或者整理功能.如果有,那么內(nèi)存就比較規(guī)整.否則如果沒有,創(chuàng)建對象就需要采用空閑列表的方式.
比如:serial,ParNew 等帶有整理的收集器,可以使用指針碰撞.CMS 使用簡單清除的算法,可以使用空閑列表.
但對象的創(chuàng)建是頻繁的,在并發(fā)的情況,多線程不一定是安全的,即存在 A 對象在分配內(nèi)存,指針還未來得及修改,B 對象也同時(shí)使用了原來的指針來分配對象。所以又衍生了兩種解決辦法,CAS+失敗重試 和 TLAB 兩種方式
CAS+失敗重試:虛擬機(jī)采用 CAS 配上失敗重試的方式保證更新操作的原子性 (關(guān)于 CAS 鎖,是樂觀鎖的一種實(shí)現(xiàn),解釋起來也比較麻煩,
TLAB:本地線程分配緩沖,把內(nèi)存分配的動作按照線程分配劃分在不同的空間中進(jìn)行,即每個(gè)線程在 Java 堆中預(yù)先分配一小塊內(nèi)存,哪個(gè)線程需要需要分配,先在 TLAB 中分配,用完了并重新分配新的 TLAB 時(shí),才需要同步鎖定。
3、初始值為零
在內(nèi)存分配完成之后,虛擬機(jī)需要將分配到的內(nèi)存空間初始化為零值 (除對象頭外),這一步操作也保證了對象的實(shí)例字段在 java 代碼中可以不賦初始值就可以使用,因?yàn)槌绦蚰茉L問這些字段的數(shù)據(jù)類型所對應(yīng)的零值。
4、設(shè)置對象頭
初始值設(shè)置之后,怎么知道對象是哪個(gè)類的實(shí)例,如何才能找到類的元數(shù)據(jù)信息、哈希碼、GC 分代年齡等信息呢?這就需要對對象頭進(jìn)行一些必要的設(shè)置,才能定位到。
5、入棧、執(zhí)行 init 指令
從虛擬機(jī)來看,對象已經(jīng)分配產(chǎn)生完成了,且入棧了;但 Java 程序來看,這才剛開始,所以,new 之后,則執(zhí)行 init 方法,進(jìn)行初始化。
6、Java 對象的內(nèi)存分布(即實(shí)例化后的對象在堆中的分布)
對象在內(nèi)存中的存儲布局可分為 3 部分:
對象頭
其中對象頭又可以細(xì)分為兩部分:
1、存儲對象自身運(yùn)行時(shí)數(shù)據(jù):如哈希碼、GC 分代年齡、鎖狀態(tài)標(biāo)志、線程持有的、偏向線程 ID 等信息
2、類型指針:即對象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè)來確定這個(gè)對象是哪個(gè)類的實(shí)例(比如是指向棧中的類聲明)
實(shí)例數(shù)據(jù)
是對象真正存儲的有效信息,比如程序中定義的各種類型的字段內(nèi)容,無論父類和子類都會記錄下來;在分配時(shí),相同寬度的字段會被分配到一起,這也是父類定義的變量會出現(xiàn)在子類之前的原因。
對齊填充
沒啥實(shí)際意義,就是為了保證對象是 8 個(gè)字節(jié)的整數(shù)倍,沒對齊時(shí),用來補(bǔ)全而已。
7、對象的訪問定位
使用對象時(shí),通過棧上的 reference 數(shù)據(jù)來操作堆上的具體對象。
建立對象是為了使用對象,Java 程序需要通過棧上的 reference 數(shù)據(jù)來操作堆上的具體對象;但這些訪問方式取決于虛擬機(jī)實(shí)現(xiàn)而定,目前主流有句柄和直接指針兩種:
句柄:從 Java 堆中劃分出一塊內(nèi)存用來作為句柄池,reference 中存儲的就是對象的句柄地址,而句柄包含了對象實(shí)例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址信息,如下圖(圖片來自 Java 虛擬機(jī)第三版)
直接指針:在直接指針中,reference 儲存的就是對象地址,所以,需要考慮的是如何防止訪問類型數(shù)據(jù)的相關(guān)信息(圖片來自 Java 虛擬機(jī)第三版)
優(yōu)點(diǎn)介紹:
句柄:使用句柄好處是,reference 中存放的是文檔的句柄地址,對象被移動時(shí),只改變句柄的實(shí)例數(shù)據(jù)指針,而 reference 本身不需要修改
直接指針:使用直接指針的最大好處就是速度更快,節(jié)省了指針定位的開銷;
HotSpot 使用第二種方式進(jìn)行對象訪問的.
三、對象的具體實(shí)例化過程
1、 在堆內(nèi)存中開辟一塊空間
2、 開辟空間分配一個(gè)地址(指針碰撞或者空閑列表兩種分配方式)
3、把對象的所有非靜態(tài)成員加載到所開辟的空間下(從方法區(qū)的非靜態(tài)區(qū)域中加載,類加載的時(shí)候.class 文件的非靜態(tài)內(nèi)容就是加載到這里的)
4、 所有的非靜態(tài)成員加載完成之后,對所有非靜態(tài)成員變量進(jìn)行默認(rèn)初始化
5、 所有非靜態(tài)成員變量默認(rèn)初始化完成之后,調(diào)用構(gòu)造函數(shù)
6、 在構(gòu)造函數(shù)入棧執(zhí)行時(shí),分為兩部分:先執(zhí)行構(gòu)造函數(shù)中的隱式三步,再執(zhí)行構(gòu)造函數(shù)中書寫的代碼:.1、隱式三步:1、執(zhí)行 super 語句,2、對開辟空間下的所有非靜態(tài)成員變量進(jìn)行顯式初始化3、執(zhí)行構(gòu)造代碼塊(注:代碼塊與非靜態(tài)成員變量顯示初始化無先后順序,與代碼順序相關(guān),如代碼塊在上,則先加載代碼塊),4、在隱式三步執(zhí)行完之后,執(zhí)行構(gòu)造函數(shù)中書寫的代碼
7、在整個(gè)構(gòu)造函數(shù)執(zhí)行完并彈棧后,把空間分配的地址賦值給一個(gè)引用對象(對象的訪問定位有句柄和直接指針兩種方式)
至此,Java 堆中有一塊內(nèi)存新的內(nèi)存 存儲這個(gè)實(shí)例化的對象,對象里面包含了對象頭、實(shí)例數(shù)據(jù)以及對齊填充。其中對象頭又可以細(xì)分為兩部分:
1、存儲對象自身運(yùn)行時(shí)數(shù)據(jù):如哈希碼、GC 分代年齡、鎖狀態(tài)標(biāo)志、線程持有的、偏向線程 ID 等信息
2、類型指針:即對象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè)來確定這個(gè)對象是哪個(gè)類的實(shí)例(比如是指向棧中的類聲明)
實(shí)例數(shù)據(jù)是對象真正存儲的有效信息。對齊填充沒什么大用處。
更多關(guān)于“Java培訓(xùn)”的問題,歡迎咨詢千鋒教育在線名師。千鋒已有十余年的培訓(xùn)經(jīng)驗(yàn),課程大綱更科學(xué)更專業(yè),有針對零基礎(chǔ)的就業(yè)班,有針對想提升技術(shù)的好程序員班,高品質(zhì)課程助理你實(shí)現(xiàn)java程序員夢想。