JVM02-JVM的對象創(chuàng)建以及訪問方式
文章目錄
前言
對象創(chuàng)建
1.類加載檢查
2.分配內(nèi)存
分配內(nèi)存的方式
內(nèi)存分配的并發(fā)問題
3.初始化零值
4.設(shè)置對象頭:
5. 執(zhí)行init方法;
對象內(nèi)存布局
對象頭
實(shí)例數(shù)據(jù)
對齊填充
對象訪問方式
使用句柄訪問
使用直接指針訪問
下面舉例說明:
總結(jié)
參考
前言
上一篇我們介紹了JVM的內(nèi)存區(qū)域布局,并且重點(diǎn)介紹了堆和棧的概念。,今天我們接著來學(xué)習(xí)JVM的對象創(chuàng)建過程已經(jīng)對象的訪問方式。
對象創(chuàng)建
對象的創(chuàng)建共有如上五個步驟:
1.類加載檢查
虛擬機(jī)遇到一條new指令時,首先將去檢查這個指令是否在常量池中定位到這個類的符號引用,并且檢查這個符號引用代表的類是否已經(jīng)被加載過、解析和初始化過。如果沒有,那必須先執(zhí)行相應(yīng)的類加載過程。JVM中類加載是通過雙親委派模型來完成的雙親委派模型加載類。
2.分配內(nèi)存
類加載檢查通過后,接下來虛擬機(jī)將為新生成對象分配內(nèi)存。對象所需的內(nèi)存大小在類加載完成后便可確定,為對象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來。分配方式有"指針碰撞"和"空閑列表"兩種,選擇那種分配方式由Java堆是否規(guī)整決定。而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。
分配內(nèi)存的方式
指針碰撞
通過"指針碰撞"分配內(nèi)存的方式適用場合是
堆內(nèi)存規(guī)整(即沒有內(nèi)存碎片) 的情況下。它的實(shí)現(xiàn)原理是:用過的內(nèi)存全部整合到一邊,沒有用到的內(nèi)存放在另一邊,只需要向著沒用過的內(nèi)存方向?qū)⒃撝羔樢苿訉ο髢?nèi)存大小位置即可。使用這種方式分配內(nèi)存的垃圾收集器有:Serial收集器和ParNew收集器。
空閑列表
通過"空閑列表"分配內(nèi)存的方式適用場景是 堆內(nèi)存不規(guī)整的情況。它的實(shí)現(xiàn)原理是:虛擬機(jī)會維護(hù)一個列表,該列表中會記錄哪些內(nèi)存塊是可用的,在分配的時候,找一塊足夠大的內(nèi)存塊來劃分對象實(shí)例,最后更新列表記錄 。使用這種方式分配內(nèi)存的垃圾收集器有:CMS收集器。
內(nèi)存分配的并發(fā)問題
在實(shí)際項(xiàng)目中,創(chuàng)建對象是很頻繁的事情,虛擬機(jī)采用兩種方式來保證線程安全:
CAS+失敗重試: CAS是樂觀鎖一種實(shí)現(xiàn)方式所謂樂觀鎖就是,每次不加鎖而是假設(shè)沒有沖突去完成某項(xiàng)操作,如果失敗就進(jìn)行重試操作,直到重試成功。虛擬機(jī)采用CAS加上失敗重試的方式保證更新操作的原子性。
TLAB: 為每一個線程預(yù)先在Eden區(qū)分配一塊內(nèi)存,JVM在給線程中的對象分配內(nèi)存時,首先在TLAB分配,當(dāng)對象大于TLAB中剩余內(nèi)存或TLAB的內(nèi)存已用盡時,再采用上述的CAS進(jìn)行內(nèi)存分配。
3.初始化零值
內(nèi)存分配完成之后,虛擬機(jī)需要將分配到的內(nèi)存空間都初始化為零值(不包括對象頭),這一步操作保證了對象的實(shí)例字段在Java代碼中可以不賦初始值就直接使用程序能訪問到這些字段的數(shù)據(jù)類型所對應(yīng)的零值。
4.設(shè)置對象頭:
初始化零值完成之后,虛擬機(jī)要對對象進(jìn)行必要的設(shè)置,例如這個對象是哪個類的實(shí)例、如何才能找到類的元數(shù)據(jù)信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象頭中。另外,根據(jù)虛擬機(jī)當(dāng)前運(yùn)行狀態(tài)的不同,是否啟用偏向鎖等,對象頭會有不同的設(shè)置方式。
5. 執(zhí)行init方法;
在上面工作都完成之后,從虛擬機(jī)的視角來看,一個新的對象已經(jīng)產(chǎn)生了,但是從Java程序的視角來看,對象創(chuàng)建才剛開始,init方法還沒有執(zhí)行,所有的字段都還為零,所有一般來說,執(zhí)行new指令之后會接著執(zhí)行init方法,把對象按照程序員的意愿進(jìn)行初始化,這樣一個真正可用的對象才算完成產(chǎn)生出來。
對象內(nèi)存布局
對象在內(nèi)存中的布局可以分3塊區(qū)域:對象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)和對齊填充(Padding)。
對象頭
虛擬機(jī)對象的對象頭部分包括兩類信息。
第一類是用于存儲對象自身的運(yùn)行時數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時間戳等長度在32位和64位的虛擬機(jī)中分別為32bit和64個bit,官方稱為"Mark Word"。
例如在32位的HotSpot虛擬機(jī)中,如對象未被同步鎖鎖定的狀態(tài)下,Mark
Word的32個比特存儲空間中的25個比特用于存儲對象哈希碼,4個比特用于存儲對象分代年齡,2個比特用于存儲鎖標(biāo)志位,1個比特固定為0狀態(tài)(輕量級鎖定、重量級鎖定、GC標(biāo)記、可偏向)下對象的存儲內(nèi)容如下所示:
對象頭的另外一部分是類型指針,即對象指向他的類型元數(shù)據(jù)的指針,Java虛擬機(jī)通過這個指針來確定該對象是哪個類的實(shí)例。并不是所有的虛擬機(jī)實(shí)現(xiàn)都必須在對象數(shù)據(jù)上保留類型指針。還一句話說,查找對象的信息并不一定要經(jīng)過對象本身,比如,如果對象是一個Java數(shù)組,那在對象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù),因?yàn)樘摂M機(jī)可以通過普通Java對象的元數(shù)據(jù)信息可以確定Java對象的大小,但是如果數(shù)組的長度是確定的,將無法通過元數(shù)據(jù)中的信息推斷數(shù)組的大小。
實(shí)例數(shù)據(jù)
實(shí)例數(shù)據(jù)部分就是對象真正存儲的有效信息,我們在程序代碼里所定義的各種類型的字段內(nèi)容,無論是從父類繼承下來的,還是在子類中定義的字段都必須記錄下來。這部分的存儲順序會受到虛擬機(jī)分配策略參數(shù)(-XX:
FieldsAllocationStyle參數(shù))和字段在Java源碼中定義的順序的影響。虛擬機(jī)默認(rèn)的分配順序?yàn)?
longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object
Pointers,OOPs)
,從以上默認(rèn)的分配策略中可以看到,相同寬度的字段總是被分配到一起存放,在滿足這個前提條件的情況下,在父類中定義的變量會出現(xiàn)在子類之前。如果虛擬機(jī)的+XX:
CompactFields參數(shù)值為true(默認(rèn)就為true),那子類之中叫窄的變量也允許插入父類變量的空隙之中,以節(jié)省一點(diǎn)點(diǎn)空間。
對齊填充
對齊填充這部分不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用。
對象訪問方式
我們的Java程序會通過棧上的reference數(shù)據(jù)來操作堆上的具體對象。對象的訪問方式也是由虛擬機(jī)實(shí)現(xiàn)的,主流的訪問方式主要有使用句柄和直接指針兩種。
使用句柄訪問
使用句柄訪問的話,Java堆中將會劃分出一塊內(nèi)存作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實(shí)例數(shù)據(jù)與類型數(shù)據(jù)各自具體的地址信息。其結(jié)構(gòu)如下圖所示:
使用直接指針訪問
使用直接指針訪問的話,Java堆中對象的內(nèi)存布局就必須考慮如何訪問類型數(shù)據(jù)的相關(guān)信息,reference中存儲的直接就是對象地址,如果只是訪問對象本身的話,就不需要多一次間接的訪問的開銷。HosSpot而言,主要使用的是這種訪問方式。其結(jié)構(gòu)如下圖所示:
使用直接指針來訪問最大的好處就是速度更快,它節(jié)省了一次指針定位的時間開銷,由于對象訪問在Java中非常頻繁,因此這類開銷積少成多也是一項(xiàng)極為可觀的執(zhí)行成本。
下面舉例說明:
Student stu =new Student(“張三”,“18”);
1
- 1
當(dāng)我們拿到stu對象時,直接調(diào)用stu.getName();時,其實(shí)就完成了對對象的訪問,這里stu只是一個變量,變量里存儲的是指向?qū)ο蟮闹羔?,?dāng)我們調(diào)用stu.getName()時,虛擬機(jī)會根據(jù)指針找到堆里面的對象然后拿到實(shí)例數(shù)據(jù)name,需要注意的是,當(dāng)我們調(diào)用stu.getClass()時,虛擬機(jī)會首先根據(jù)stu指針定位到堆里面的對象,然后根據(jù)對象頭里面存儲的指向Class類元信息的指針再次到方法區(qū)拿到Class對象,進(jìn)行了兩次指針尋找。具體講解圖如下:
總結(jié)
本文首先介紹了JVM中對象的創(chuàng)建過程,接著就是介紹對象的內(nèi)存布局,最后就是說到了對象的訪問方式,其中對象的創(chuàng)建過程比較重要的一塊內(nèi)容就是分配內(nèi)存。
作者:碼農(nóng)飛哥
微信公眾號:碼農(nóng)飛哥
