JVM - 写了这么多年代码,你还不知道new对象背后的逻辑?
文章目錄
- 對(duì)象創(chuàng)建流程
- 【類(lèi)加載檢查】
- 【分配內(nèi)存】
- 內(nèi)存劃分的兩種方式
- 解決分配內(nèi)存并發(fā)問(wèn)題的兩種方式
- 【初始化】
- 【設(shè)置對(duì)象頭】
- 對(duì)象的組成
- 對(duì)象頭的兩部分組成
- 【執(zhí)行init方法】
- 總結(jié)一下
對(duì)象創(chuàng)建流程
我們知道JVM三大組成部分: 類(lèi)加載子系統(tǒng)、運(yùn)行時(shí)數(shù)據(jù)區(qū) 、字節(jié)碼執(zhí)行引擎。
要想new 一個(gè)對(duì)象,肯定是要繞不開(kāi)JVM的機(jī)制。
【類(lèi)加載檢查】
JVM啟動(dòng)的時(shí)候并不是將所有的類(lèi)都初始化,所以當(dāng)碰到一個(gè)new指令時(shí),JVM首先會(huì)去檢查這個(gè)類(lèi)有沒(méi)有被加載,具體就是去常量池中看是否有這個(gè)類(lèi)的符號(hào)引用,并檢查這個(gè)符號(hào)引用代表的類(lèi)是否已經(jīng)被加載、解析和初始化過(guò) 。 若沒(méi)有這必須經(jīng)歷【類(lèi)加載子系統(tǒng)】的歷練 (加載–校驗(yàn)–準(zhǔn)備–解析–初始化)
JVM-白話聊一聊JVM類(lèi)加載和雙親委派機(jī)制源碼解析
【分配內(nèi)存】
類(lèi)加載校驗(yàn)通過(guò)后 ,是不是該分配內(nèi)存了呢?
是的, 接下來(lái)JVM將會(huì)為這個(gè)新生的對(duì)象分給內(nèi)存,因?yàn)檫@個(gè)新生對(duì)象所需要內(nèi)存大小在類(lèi)加載完之后便可以完全確定,對(duì)象放哪里呢? 通常都是放在堆中,所以所謂的分配內(nèi)存實(shí)際上就是從Java堆中劃分出一塊固定大小的內(nèi)存給這個(gè)新生對(duì)象。
雖然很簡(jiǎn)單的一件事情,但是要考慮的地方可不少
內(nèi)存劃分的兩種方式
JVM提供了2中劃分內(nèi)存的方法
- 指針碰撞(Bump the Pointer) 【默認(rèn)方式】
如果堆中的內(nèi)存是絕對(duì)規(guī)整的,大家都按順序排放,分配過(guò)內(nèi)存的對(duì)象那個(gè)在一邊,未使用的內(nèi)存在另外一邊 ,分界線使用指針來(lái)維護(hù)。因?yàn)樾律鷮?duì)象所需要內(nèi)存大小在類(lèi)加載完之后便可以完全確定,所以僅需要將指針移動(dòng)對(duì)象大小的位置即可。
當(dāng)然了這是一種理想的情況,JVM里還有GC,會(huì)標(biāo)記清除等等
-
空閑列表(Free List)
如果堆內(nèi)存中的內(nèi)存并不是規(guī)整的,分配的內(nèi)存和未分配的內(nèi)存糅雜在一起, 如果還用上面的指針碰撞的方式, 如果移動(dòng)的可用內(nèi)存無(wú)法容納這個(gè)對(duì)象,放不下啊? 咋弄? 繼續(xù)碰么?
顯然效率很低。 所以JVM采用了另外一種方式,JVM維護(hù)了一個(gè)列表,記錄了堆中的可用內(nèi)存,那么分配內(nèi)存的時(shí)候就從JVM維護(hù)的列表中找一個(gè)足夠容納這個(gè)對(duì)象的內(nèi)存區(qū)域給它,并更新列表記錄。
解決分配內(nèi)存并發(fā)問(wèn)題的兩種方式
第二個(gè)問(wèn)題 并發(fā)問(wèn)題如何解決呢?
在并發(fā)的情況下,可能出現(xiàn)JVM正在給對(duì)象A分配內(nèi)存,但是指針還沒(méi)來(lái)得及修改,對(duì)象B又使用了A的內(nèi)存空間的情況。
為了解決這個(gè)問(wèn)題,JVM采取了
- CAS (compare and swap)
簡(jiǎn)而言之就是JVM采用【 CAS+失敗重試 】保證更新操作的原子性 。
- 本地線程分配緩沖 (Thread Local Allocation Buffer , TLAB)
把內(nèi)存分配的動(dòng)作按照線程劃分在不同的空間之中進(jìn)行,即每個(gè)線程在Java堆中預(yù)先分配一小塊內(nèi)存
通過(guò)-XX:+/- UseTLAB參數(shù)來(lái)設(shè)定虛擬機(jī)是否使用TLAB。
JDK8中默認(rèn)開(kāi)啟XX:+UseTLAB ,默認(rèn)值eden區(qū)域的1%,當(dāng)然了也可以通過(guò)-XX:TLABSize 指定TLAB大小 。 一般不建議修改。
如果TLAB還放不下,那就走CAS了…
不管怎么分配,目的只是為了更好的回收內(nèi)存或者更快的分配對(duì)象
【初始化】
內(nèi)存分配完成后,虛擬機(jī)需要將分配到的內(nèi)存空間都初始化為零值(不包括對(duì)象頭).
如果使用TLAB,這一工作過(guò)程也可以提前至TLAB分配時(shí)進(jìn)行。
這一步操作保證了對(duì)象的實(shí)例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問(wèn)到這些字段的數(shù)據(jù)類(lèi)型所對(duì)應(yīng)的默認(rèn)值 (比如 int 默認(rèn)0 , String 默認(rèn)null , boolean 默認(rèn)false等等)
【設(shè)置對(duì)象頭】
初始化默認(rèn)值以后,JVM要對(duì)對(duì)象進(jìn)行必要的設(shè)置,例如這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例、如何才能找到類(lèi)的元數(shù)據(jù)信息、對(duì)象的哈希碼、對(duì)象的GC分代年齡等信息。這些信息存放在對(duì)象的對(duì)象頭Object Header之中。
這部分?jǐn)?shù)據(jù)的長(zhǎng)度在32位和64位的虛擬機(jī)中分別為32個(gè)和64個(gè)bits,官方稱(chēng)它為“Mark Word”。
對(duì)象的組成
在HotSpot虛擬機(jī)中,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為3塊區(qū)域:對(duì)象頭(Header)、 實(shí)例數(shù)據(jù)(Instance Data)和和對(duì)齊填充(Padding) 。
對(duì)象頭的兩部分組成
HotSpot虛擬機(jī)的對(duì)象頭包括兩部分信息
- 第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù), 如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時(shí) 間戳等。
32位操作系統(tǒng)為例
- 對(duì)象頭的另外一部分是類(lèi)型指針,即對(duì)象指向它的類(lèi)元數(shù)據(jù)的指針,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例。
如下所示
【執(zhí)行init方法】
執(zhí)行方法,即對(duì)象按照程序員的意愿進(jìn)行初始化。對(duì)應(yīng)到語(yǔ)言層面上講,就是為屬性賦值(注意,這與上面的賦零值不同,這是由程序員賦的值) 和執(zhí)行構(gòu)造方法。
IDEA安裝jclasslib插件可以查看
這里的init實(shí)際上是C++調(diào)用的,相對(duì)于面向開(kāi)發(fā)人員 就是 new Artisan() ,并執(zhí)行Artisan默認(rèn)的構(gòu)造函數(shù)。
總結(jié)一下
總結(jié)
以上是生活随笔為你收集整理的JVM - 写了这么多年代码,你还不知道new对象背后的逻辑?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: JVM - 应用JVM核心参数推荐设置
- 下一篇: JVM - 剖析Java对象头Objec