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