溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Java 對(duì)象的創(chuàng)建過(guò)程

發(fā)布時(shí)間:2020-07-05 14:25:21 來(lái)源:網(wǎng)絡(luò) 閱讀:505 作者:專注地一哥 欄目:編程語(yǔ)言

類的初始化與實(shí)例化
一個(gè) Java 對(duì)象的創(chuàng)建過(guò)程往往包括類的初始化 和 實(shí)例化 兩個(gè)階段。
Java 規(guī)范規(guī)定一個(gè)對(duì)象在可以被使用之前必須要被正確地初始化。在類初始化過(guò)程中或初始化完畢后,根據(jù)具體情況才會(huì)去對(duì)類進(jìn)行實(shí)例化。在實(shí)例化一個(gè)對(duì)象時(shí),JVM 首先會(huì)檢查相關(guān)類型是否已經(jīng)加載并初始化,如果沒(méi)有,則 JVM 立即進(jìn)行加載并調(diào)用類構(gòu)造器完成類的初始化。
Java 對(duì)象的創(chuàng)建方式
一個(gè)對(duì)象在可以被使用之前必須要被正確地實(shí)例化。在 Java 程序中,有多種方法可以創(chuàng)建對(duì)象,最直接的一種就是使用 new 關(guān)鍵字來(lái)調(diào)用一個(gè)類的構(gòu)造函數(shù)顯式地創(chuàng)建對(duì)象。這種方式是由執(zhí)行類的實(shí)例創(chuàng)建表達(dá)式創(chuàng)建對(duì)象。除此之外,還可以使用反射機(jī)制 (Class 類的 newInstance 方法、Constructor 類的newInstance 方法)、使用 Clone 方法、使用反序列化等方式創(chuàng)建對(duì)象。
使用 new 關(guān)鍵字創(chuàng)建對(duì)象
這是最常見(jiàn)、最簡(jiǎn)單的創(chuàng)建對(duì)象的方式,通過(guò)這種方式可以調(diào)用任意的構(gòu)造函數(shù)(無(wú)參的和有參的)創(chuàng)建對(duì)象。
使用 Class 類的 newInstance 方法 (反射機(jī)制) 。事實(shí)上 Class 類的 newInstance 方法內(nèi)部調(diào)用的是 Constructor 類的 newInstance 方法,相當(dāng)于是調(diào)用無(wú)參的構(gòu)造器創(chuàng)建對(duì)象。
使用 Constructor 類的 newInstance 方法 (反射機(jī)制) 。該方法和 Class 類中的 newInstance 方法類似,不同的是 Constructor 類的 newInstance 方法可以調(diào)用有參數(shù)的和私有的構(gòu)造函數(shù)。
使用Clone方法創(chuàng)建對(duì)象
調(diào)用一個(gè)對(duì)象的 clone 方法,JVM 都會(huì)創(chuàng)建一個(gè)新的、一樣的對(duì)象。特別需要說(shuō)明的是,用 clone 方法創(chuàng)建對(duì)象的過(guò)程中并不會(huì)調(diào)用任何構(gòu)造函數(shù)。如何使用 clone 方法以及淺克隆/深克隆機(jī)制。簡(jiǎn)單而言,要想使用 clone 方法,就必須先實(shí)現(xiàn) Cloneable 接口并實(shí)現(xiàn)其定義的 clone 方法,這也是原型模式的應(yīng)用。
使用 (反) 序列化機(jī)制創(chuàng)建對(duì)象
當(dāng)反序列化一個(gè)對(duì)象時(shí),JVM會(huì)創(chuàng)建一個(gè)單獨(dú)的對(duì)象,在此過(guò)程中,JVM并不會(huì)調(diào)用任何構(gòu)造函數(shù)。為了反序列化一個(gè)對(duì)象,對(duì)應(yīng)的類需要實(shí)現(xiàn) Serializable 接口。
從 Java 虛擬機(jī)層面看,除了使用 new 關(guān)鍵字創(chuàng)建對(duì)象的方式外,其他方式全部都是通過(guò)轉(zhuǎn)變?yōu)?invokevirtual 指令直接創(chuàng)建對(duì)象的。
Java 對(duì)象的創(chuàng)建過(guò)程
當(dāng)一個(gè)對(duì)象被創(chuàng)建時(shí),虛擬機(jī)就會(huì)為其分配內(nèi)存來(lái)存放對(duì)象自己的實(shí)例變量及其繼承父類的實(shí)例變量 (即使繼承超類的實(shí)例變量有可能被隱藏也會(huì)被分配空間) 。在為這些實(shí)例變量分配內(nèi)存的同時(shí),這些實(shí)例變量也會(huì)被賦予默認(rèn)值。在內(nèi)存分配完成之后,Java 虛擬機(jī)就會(huì)開(kāi)始對(duì)新創(chuàng)建的對(duì)象進(jìn)行初始化。在 Java 對(duì)象初始化過(guò)程中,主要涉及三種執(zhí)行對(duì)象初始化的結(jié)構(gòu),分別是實(shí)例變量初始化、實(shí)例代碼塊初始化以及構(gòu)造函數(shù)初始化。
實(shí)例變量初始化與實(shí)例代碼塊初始化
在定義(聲明)實(shí)例變量的同時(shí),可以直接對(duì)實(shí)例變量進(jìn)行賦值或者使用實(shí)例代碼塊對(duì)其進(jìn)行賦值。如果以這兩種方式為實(shí)例變量進(jìn)行初始化,那么它們將在構(gòu)造函數(shù)執(zhí)行之前完成這些初始化操作。實(shí)際上,如果對(duì)實(shí)例變量直接賦值或者使用實(shí)例代碼塊賦值,那么編譯器會(huì)將其中的代碼放到類的構(gòu)造函數(shù)中去,并且這些代碼會(huì)被放在對(duì)超類構(gòu)造函數(shù)的調(diào)用語(yǔ)句之后 (構(gòu)造函數(shù)的第一條語(yǔ)句必須是超類構(gòu)造函數(shù)的調(diào)用語(yǔ)句) ,構(gòu)造函數(shù)本身的代碼之前。
特別需要注意的是,Java 是按照先后順序來(lái)執(zhí)行實(shí)例變量初始化和實(shí)例初始化器中的代碼,并且不允許順序靠前的實(shí)例代碼塊初始化在其后面定義的實(shí)例變量。這么做是為了保證一個(gè)變量在被使用之前已經(jīng)被正確地初始化。
構(gòu)造函數(shù)初始化
實(shí)例變量初始化與實(shí)例代碼塊初始化總是發(fā)生在構(gòu)造函數(shù)初始化之前。Java 中的每一個(gè)類中都至少會(huì)有一個(gè)構(gòu)造函數(shù),如果沒(méi)有顯式定義構(gòu)造函數(shù),那么 JVM 會(huì)為它提供一個(gè)默認(rèn)無(wú)參的構(gòu)造函數(shù)。在編譯生成的字節(jié)碼中,這些構(gòu)造函數(shù)會(huì)被命名成 () 方法 (參數(shù)列表與 Java 語(yǔ)言中構(gòu)造函數(shù)的參數(shù)列表相同) 。Java 要求在實(shí)例化類之前,必須先實(shí)例化其超類,以保證所創(chuàng)建實(shí)例的完整性。
事實(shí)上,這一點(diǎn)是在構(gòu)造函數(shù)中保證的:Java 強(qiáng)制要求除 Object 類 (Object 是 Java 的頂層類,沒(méi)有超類) 之外所有類的構(gòu)造函數(shù)中的第一條語(yǔ)句必須是超類構(gòu)造函數(shù)的調(diào)用語(yǔ)句或者是類中定義的其他的構(gòu)造函數(shù)。如果既沒(méi)有調(diào)用其他的構(gòu)造函數(shù),也沒(méi)有顯式調(diào)用超類的構(gòu)造函數(shù),那么編譯器會(huì)自動(dòng)生成一個(gè)對(duì)超類構(gòu)造函數(shù)的調(diào)用。
如果顯式調(diào)用超類的構(gòu)造函數(shù),那么該調(diào)用必須放在構(gòu)造函數(shù)所有代碼的最前面。正因?yàn)槿绱?,Java 才可以使得一個(gè)對(duì)象在初始化之前其所有的超類都被初始化完成,并保證創(chuàng)建一個(gè)完整的對(duì)象出來(lái)。特別地,如果在一個(gè)構(gòu)造函數(shù)中調(diào)用另外一個(gè)構(gòu)造函數(shù)則不能顯式調(diào)用超類的構(gòu)造函數(shù),而且要另一個(gè)構(gòu)造函數(shù)放在構(gòu)造函數(shù)所有代碼的最前面。
Java 通過(guò)對(duì)構(gòu)造函數(shù)作出上述限制保證一個(gè)類的實(shí)例能夠在被使用之前正確地初始化。
1.Java普通對(duì)象的創(chuàng)建
這里討論的僅僅是普通Java對(duì)象,不包含數(shù)組和Class對(duì)象。
1.1new指令
虛擬機(jī)遇到一條new指令時(shí),首先去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類的符號(hào)引用,并且檢查這個(gè)符號(hào)引用代表的類是否已被加載、解析和初始化過(guò)。如果沒(méi)有,那么須先執(zhí)行相應(yīng)的類加載過(guò)程。
1.2分配內(nèi)存
接下來(lái)虛擬機(jī)將為新生代對(duì)象分配內(nèi)存。對(duì)象所需的內(nèi)存的大小在類加載完成后便可完全確定。分配方式有“指針碰撞(Bump the Pointer)”和“空閑列表(Free List)”兩種方式,具體由所采用的垃圾收集器是否帶有壓縮整理功能決定。
1.3初始化
內(nèi)存分配完成后,虛擬機(jī)需要將分配到的內(nèi)存空間都初始化為零值(不包括對(duì)象頭),這一步操作保證了對(duì)象的實(shí)例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問(wèn)到這些字段的數(shù)據(jù)類型所對(duì)應(yīng)的零值。
1.4對(duì)象的初始設(shè)置
接下來(lái)虛擬機(jī)要對(duì)對(duì)象進(jìn)行必要的設(shè)置,例如這個(gè)對(duì)象是哪個(gè)類的實(shí)例、如何才能找到類的元數(shù)據(jù)信息、對(duì)象的哈希碼、對(duì)象的GC分代年齡等信息。這些信息存放在對(duì)象的對(duì)象頭(Object Header)之中。根據(jù)虛擬機(jī)當(dāng)前的運(yùn)行狀態(tài)的不同,如對(duì)否啟用偏向鎖等,對(duì)象頭會(huì)有不同的設(shè)置方式。
1.5<init>方法
在上面的工作都完成了之后,從虛擬機(jī)的角度看,一個(gè)新的對(duì)象已經(jīng)產(chǎn)生了,但是從Java程序的角度看,對(duì)象創(chuàng)建才剛剛開(kāi)始—<init>方法還沒(méi)有執(zhí)行,所有的字段都還為零。所以,一般來(lái)說(shuō),執(zhí)行new指令后悔接著執(zhí)行init方法,把對(duì)象按照程序員的意愿進(jìn)行初始化(應(yīng)該是將構(gòu)造函數(shù)中的參數(shù)賦值給對(duì)象的字段),這樣一個(gè)真正可用的對(duì)象才算完全產(chǎn)生出來(lái)。
2.Java對(duì)象內(nèi)存布局
在HotSpot虛擬機(jī)中,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為3塊區(qū)域:對(duì)象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)、對(duì)其填充(Padding)。
2.1對(duì)象頭
HotSpot虛擬機(jī)的對(duì)象頭包含兩部分信息,第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時(shí)間戳等。
對(duì)象的另一部分類型指針,即對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例(并不是所有的虛擬機(jī)實(shí)現(xiàn)都必須在對(duì)象數(shù)據(jù)上保留類型指針,也就是說(shuō),查找對(duì)象的元數(shù)據(jù)信息并不一定要經(jīng)過(guò)對(duì)象本身)。
如果對(duì)象是一個(gè)Java數(shù)組,那在對(duì)象頭中還必須有一塊用于記錄數(shù)組長(zhǎng)度的數(shù)據(jù)。
元數(shù)據(jù):描述數(shù)據(jù)的數(shù)據(jù)。對(duì)數(shù)據(jù)及信息資源的描述信息。在Java中,元數(shù)據(jù)大多表示為注解。
2.2實(shí)例數(shù)據(jù)
實(shí)例數(shù)據(jù)部分是對(duì)象真正存儲(chǔ)的有效信息,也是在程序代碼中定義的各種類型的字段內(nèi)容,無(wú)論從父類繼承下來(lái)的,還是在子類中定義的,都需要記錄起來(lái)。這部分的存儲(chǔ)順序會(huì)虛擬機(jī)默認(rèn)的分配策略參數(shù)和字段在Java源碼中定義的順序影響(相同寬度的字段總是被分配到一起)。
2.3對(duì)齊填充
對(duì)齊填充部分并不是必然存在的,也沒(méi)有特別的含義,它僅僅起著占位符的作用。由于HotSpot VM的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象的起始地址必須是8字節(jié)的整數(shù)倍,也就是說(shuō),對(duì)象的大小必須是8字節(jié)的整數(shù)倍。而對(duì)象頭部分正好是8字節(jié)的倍數(shù)(1倍或者2倍),因此,當(dāng)對(duì)象實(shí)例數(shù)據(jù)部分沒(méi)有對(duì)齊時(shí),就需要通過(guò)對(duì)齊填充來(lái)補(bǔ)全。
大家都知道,java使用new 關(guān)鍵字進(jìn)行對(duì)象的創(chuàng)建,但這只是從語(yǔ)言層次上理解了對(duì)象的創(chuàng)建,下邊我們從jvm的角度來(lái)看看,對(duì)象是怎么被創(chuàng)建出來(lái)的,即對(duì)象的創(chuàng)建過(guò)程。
對(duì)象的創(chuàng)建大概分為以下幾步:
1:檢查類是否已經(jīng)被加載;
2:為對(duì)象分配內(nèi)存空間;
3:為對(duì)象字段設(shè)置零值;
4:設(shè)置對(duì)象頭;
5:執(zhí)行構(gòu)造方法。
第一步,當(dāng)程序遇到new 關(guān)鍵字時(shí),首先會(huì)去運(yùn)行時(shí)常量池中查找該引用所指向的類有沒(méi)有被虛擬機(jī)加載,如果沒(méi)有被加載,那么會(huì)進(jìn)行類的加載過(guò)程,如果已經(jīng)被加載,那么進(jìn)行下一步,為對(duì)象分配內(nèi)存空間;
第二步,加載完類之后,需要在堆內(nèi)存中為該對(duì)象分配一定的空間,該空間的大小在類加載完成時(shí)就已經(jīng)確定下來(lái)了,這里多說(shuō)一點(diǎn),為對(duì)象分配內(nèi)存空間有兩種方式:
(1)第一種是jvm將堆區(qū)抽象為兩塊區(qū)域,一塊是已經(jīng)被其他對(duì)象占用的區(qū)域,另一塊是空白區(qū)域,中間通過(guò)一個(gè)指針進(jìn)行標(biāo)注,這時(shí)只需要將指針向空白區(qū)域移動(dòng)相應(yīng)大小空間,就完成了內(nèi)存的分配,當(dāng)然這種劃分的方式要求虛擬機(jī)的對(duì)內(nèi)存是地址連續(xù)的,且虛擬機(jī)帶有內(nèi)存壓縮機(jī)制,可以在內(nèi)存分配完成時(shí)壓縮內(nèi)存,形成連續(xù)地址空間,這種分配內(nèi)存方式成為“指針碰撞”,但是很明顯,這種方式也存在一個(gè)比較嚴(yán)重的問(wèn)題,那就是多線程創(chuàng)建對(duì)象時(shí),會(huì)導(dǎo)致指針劃分不一致的問(wèn)題,例如A線程剛剛將指針移動(dòng)到新位置,但是B線程之前讀取到的是指針之前的位置,這樣劃分內(nèi)存時(shí)就出現(xiàn)不一致的問(wèn)題,解決這種問(wèn)題,虛擬機(jī)采用了循環(huán)CAS操作來(lái)保證內(nèi)存的正確劃分;
(2)第二種也是為了解決第一種分配方式的不足而創(chuàng)建的方式,多線程分配內(nèi)存時(shí),虛擬機(jī)為每個(gè)線程分配了不同的空間,這樣每個(gè)線程在分配內(nèi)存時(shí)只是在自己的空間中操作,從而避免了上述問(wèn)題,不需要同步。當(dāng)然,當(dāng)線程自己的空間用完了才需要需申請(qǐng)空間,這時(shí)候需要進(jìn)行同步鎖定。為每個(gè)線程分配的空間稱為“本地線程分配緩沖(TLAB)”,是否啟用TLAB需要通過(guò) -XX:+/-UseTLAB參數(shù)來(lái)設(shè)定。
第三步,分配完內(nèi)存后,需要對(duì)對(duì)象的字段進(jìn)行零值初始化,對(duì)象頭除外,零值初始化意思就是對(duì)對(duì)象的字段賦0值,或者null值,這也就解釋了為什么這些字段在不需要進(jìn)程初始化時(shí)候就能直接使用;
第四步,這里,虛擬機(jī)需要對(duì)這個(gè)將要?jiǎng)?chuàng)建出來(lái)的對(duì)象,進(jìn)行信息標(biāo)記,包括是否為新生代/老年代,對(duì)象的哈希碼,元數(shù)據(jù)信息,這些標(biāo)記存放在對(duì)象頭信息中,對(duì)象頭非常復(fù)雜,這里不作解釋,可以另行百度;
第五步,也就是最后一步,執(zhí)行對(duì)象的構(gòu)造方法,這里做的操作才是程序員真正想做的操作,例如初始化其他對(duì)象啊等等操作,至此,對(duì)象創(chuàng)建成功。
java中個(gè),創(chuàng)建一個(gè)對(duì)象需要經(jīng)過(guò)五步,分別是類加載檢查、分配內(nèi)存、初始化零值、設(shè)置對(duì)象頭和執(zhí)行初始化init()。

  1. 類加載檢查
    在java中,new一個(gè)對(duì)象的時(shí)候,java虛擬機(jī)會(huì)首先去檢查這個(gè)指令的參數(shù)是否能在常量池中找到這個(gè)對(duì)象對(duì)應(yīng)的類的符號(hào)引用,檢查這個(gè)符號(hào)引用代表的類是否被類加載器加載、解析和初始化;如果沒(méi)有,則必須要進(jìn)行類加載。
  2. 分配內(nèi)存
    在類加載之后,虛擬機(jī)會(huì)為將要的ThinkMarkets代理申請(qǐng)www.kaifx.cn/broker/thinkmarkets.html創(chuàng)建的對(duì)象分配內(nèi)存,對(duì)象所需內(nèi)存的大小在類加載完成便可完全確定,給對(duì)象分配內(nèi)存是要在java堆中劃分出一塊確定的內(nèi)存。在java堆內(nèi)存分配一般有兩種方式,指針碰撞和空閑列表。
    (1)指針碰撞
    在java堆規(guī)整的情況下,適合采用指針碰撞方式。用過(guò)的內(nèi)存全部整合到以便,沒(méi)有用過(guò)的內(nèi)存放在另外一邊,中間有一個(gè)分界值指針,用來(lái)將用過(guò)的內(nèi)存與空閑內(nèi)存分隔開(kāi)來(lái),當(dāng)給新生對(duì)象分配內(nèi)存時(shí),指針便會(huì)向空閑內(nèi)存區(qū)域移動(dòng)。
    (2)空閑列表
    在java堆不規(guī)整的情況下,適合采用空閑列表方式。這種方式中,java虛擬機(jī)會(huì)維護(hù)一個(gè)列表,該列表是記錄內(nèi)存的塊是否是可用的,當(dāng)為新生對(duì)象分配內(nèi)存的時(shí)候,會(huì)找一塊足夠大的內(nèi)存分配給新生對(duì)象,之后更新這個(gè)列表。
    java堆是否規(guī)整由java虛擬機(jī)采用的垃圾收集器是否有壓縮整理的功能決定。
  3. 初始化零值
    在給新生對(duì)象分配完內(nèi)存完之后,虛擬機(jī)需要將分配到的內(nèi)存空間都初始化為零值,這步操作保證了對(duì)象的實(shí)例字段在java代碼中可以不賦初值就可以直接使用。
  4. 設(shè)置對(duì)象頭
    初始化零值之后,要對(duì)新生對(duì)象設(shè)置對(duì)象頭。對(duì)象頭中包含類的元數(shù)據(jù)信息、對(duì)象的哈希碼、對(duì)象的GC分代年齡等信息。根據(jù)虛擬機(jī)當(dāng)前運(yùn)行狀態(tài)的不同,對(duì)象頭也會(huì)有不同的設(shè)置方式。
  5. 初始化(執(zhí)行init方法)
    在給對(duì)象設(shè)置完對(duì)象頭之后,虛擬機(jī)已經(jīng)將一個(gè)對(duì)象產(chǎn)生了,此時(shí),方法沒(méi)有執(zhí)行,對(duì)象的所有字段都為零值,零值的對(duì)象在程序中沒(méi)有使用意義,只有初始化之后,對(duì)象才能真正體現(xiàn)出作用。
    此外,虛擬機(jī)創(chuàng)建java對(duì)象的時(shí)候,要保障線程的安全,虛擬機(jī)采用兩種方式來(lái)保證線程安全。
    Java創(chuàng)建對(duì)象的過(guò)程
    簡(jiǎn)單記錄一下Java創(chuàng)建對(duì)象的過(guò)程,就是new一個(gè)對(duì)象的時(shí)候發(fā)生了哪些事情。Java程序執(zhí)行的過(guò)程在此不作說(shuō)明,對(duì)象的創(chuàng)建過(guò)程只是程序執(zhí)行過(guò)程的一部分。有關(guān)整個(gè)程序執(zhí)行的過(guò)程,等熟悉了虛擬機(jī)之后在作說(shuō)明。
    對(duì)象創(chuàng)建過(guò)程簡(jiǎn)述
    Java中對(duì)象的創(chuàng)建就是在堆上分配內(nèi)存空間的過(guò)程,此處說(shuō)的對(duì)象創(chuàng)建僅限于new關(guān)鍵字創(chuàng)建的普通Java對(duì)象,不包括數(shù)組對(duì)象的創(chuàng)建。
    大致過(guò)程如下:
    檢測(cè)類是否被加載
    為對(duì)象分配內(nèi)存
    為分配的內(nèi)存空間初始化零值
    對(duì)對(duì)象進(jìn)行其他設(shè)置
    執(zhí)行init方法
    檢測(cè)類是否被加載
    當(dāng)虛擬機(jī)執(zhí)行到new時(shí),會(huì)先去常量池中查找這個(gè)類的符號(hào)引用。如果能找到符號(hào)引用,說(shuō)明此類已經(jīng)被加載到方法區(qū)(方法區(qū)存儲(chǔ)虛擬機(jī)已經(jīng)加載的類的信息),可以繼續(xù)執(zhí)行;如果找不到符號(hào)引用,就會(huì)使用類加載器執(zhí)行類的加載過(guò)程,類加載完成后繼續(xù)執(zhí)行。
    為對(duì)象分配內(nèi)存
    類加載完成以后,虛擬機(jī)就開(kāi)始為對(duì)象分配內(nèi)存,此時(shí)所需內(nèi)存的大小就已經(jīng)確定了。只需要在堆上分配所需要的內(nèi)存即可。
    具體的分配內(nèi)存有兩種情況:第一種情況是內(nèi)存空間絕對(duì)規(guī)整,第二種情況是內(nèi)存空間是不連續(xù)的。
    對(duì)于內(nèi)存絕對(duì)規(guī)整的情況相對(duì)簡(jiǎn)單一些,虛擬機(jī)只需要在被占用的內(nèi)存和可用空間之間移動(dòng)指針即可,這種方式被稱為指針碰撞。
    對(duì)于內(nèi)存不規(guī)整的情況稍微復(fù)雜一點(diǎn),這時(shí)候虛擬機(jī)需要維護(hù)一個(gè)列表,來(lái)記錄哪些內(nèi)存是可用的。分配內(nèi)存的時(shí)候需要找到一個(gè)可用的內(nèi)存空間,然后在列表上記錄下已被分配,這種方式成為空閑列表。
    分配內(nèi)存的時(shí)候也需要考慮線程安全問(wèn)題,有兩種解決方案:
    第一種是采用同步的辦法,使用CAS來(lái)保證操作的原子性。
    另一種是每個(gè)線程分配內(nèi)存都在自己的空間內(nèi)進(jìn)行,即是每個(gè)線程都在堆中預(yù)先分配一小塊內(nèi)存,稱為本地線程分配緩沖(TLAB),分配內(nèi)存的時(shí)候再TLAB上分配,互不干擾。
    為分配的內(nèi)存空間初始化零值
    對(duì)象的內(nèi)存分配完成后,還需要將對(duì)象的內(nèi)存空間都初始化為零值,這樣能保證對(duì)象即使沒(méi)有賦初值,也可以直接使用。
    對(duì)對(duì)象進(jìn)行其他設(shè)置
    分配完內(nèi)存空間,初始化零值之后,虛擬機(jī)還需要對(duì)對(duì)象進(jìn)行其他必要的設(shè)置,設(shè)置的地方都在對(duì)象頭中,包括這個(gè)對(duì)象所屬的類,類的元數(shù)據(jù)信息,對(duì)象的hashcode,GC分代年齡等信息。
    執(zhí)行init方法
    執(zhí)行完上面的步驟之后,在虛擬機(jī)里這個(gè)對(duì)象就算創(chuàng)建成功了,但是對(duì)于Java程序來(lái)說(shuō)還需要執(zhí)行init方法才算真正的創(chuàng)建完成,因?yàn)檫@個(gè)時(shí)候?qū)ο笾皇潜怀跏蓟阒盗耍€沒(méi)有真正的去根據(jù)程序中的代碼分配初始值,調(diào)用了init方法之后,這個(gè)對(duì)象才真正能使用。
    到此為止一個(gè)對(duì)象就產(chǎn)生了,這就是new關(guān)鍵字創(chuàng)建對(duì)象的過(guò)程。過(guò)程如下:
    檢測(cè)類是否被加載–>為對(duì)象分配內(nèi)存空間–>初始零值–>進(jìn)行必要的設(shè)置–>調(diào)用init方法進(jìn)行初始化。
    對(duì)象的創(chuàng)建過(guò)程:
    類加載檢查-->分配內(nèi)存-->初始化零值-->設(shè)置對(duì)象頭-->執(zhí)行init方法
    1、類加載檢查:虛擬機(jī)遇到一條new指令時(shí),先檢查這個(gè)指令的參數(shù)能否在常量池中定位到一個(gè)類的符號(hào)引用,并檢查這個(gè)符號(hào)引用代表的類是否已被ji加載、解析和初始化過(guò)。如果沒(méi)有,則先進(jìn)行類的加載過(guò)程。
    2、分配內(nèi)存:有兩種方式
    指針碰撞:假設(shè)Java堆中的內(nèi)存是規(guī)整的,用過(guò)的內(nèi)存在一邊,空閑的在另一邊,中間有一個(gè)指針作為分界點(diǎn)的指示器,所分配的內(nèi)存就把那個(gè)指針向空閑那邊挪動(dòng)一段與對(duì)象大小相等的距離。
    空閑列表:如果Java堆中的內(nèi)存不是規(guī)整的,虛擬機(jī)必須維護(hù)一個(gè)列表,記錄哪些內(nèi)存塊可用的,分配時(shí)從列表中找到一塊足夠大的空間劃分給對(duì)象,并更新列表的記錄。
    在劃分可用空間時(shí),會(huì)遇到線程安全的問(wèn)題。解決這個(gè)問(wèn)題有兩種方案。第一種:對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行同步處理--虛擬機(jī)采用CAS配上失敗重試的方式保證更新操作的原子性。另一種是把內(nèi)存分配的動(dòng)作安裝現(xiàn)場(chǎng)劃分在不同的空間之中進(jìn)行,即每個(gè)線程在Java堆中預(yù)先分配一小塊內(nèi)存,稱為本地線程分配緩沖(TLAB)。那個(gè)線程需要分配內(nèi)存,就在哪個(gè)線程的TLAB上分配,只有TLAB用完并分配新的TLAB時(shí),才需要同步鎖定。是否使用TLAB,-XX:+UseTLAB參數(shù)來(lái)設(shè)定。
    3、初始化零值 將分配到的內(nèi)存空間都初始化為零值,如果用TLAB,則在TLAB分配時(shí)初始化為零值。
    4、設(shè)置對(duì)象頭:主要設(shè)置類的元數(shù)據(jù)信息、對(duì)象的哈希碼、對(duì)象的GC分代年齡等信息。
    5、執(zhí)行init方法初始化。
    類加載過(guò)程
    當(dāng)JVM第一次要使用一個(gè)類的時(shí)候,需要加載這個(gè)類;
    首先根據(jù)classpath的配到硬盤上找這個(gè)類的class文件(如果沒(méi)有配置classpath,就到當(dāng)前位置找);
    如果找到這個(gè)class,就加載到方法區(qū);
    a) 分別將這個(gè)類的靜態(tài)成員加載到靜態(tài)區(qū)域,非靜態(tài)成員加載到非靜態(tài)區(qū)域;
    b) 在靜態(tài)區(qū)域?yàn)樗徐o態(tài)成員變量分配空間,賦默認(rèn)值;
    c) 為所有靜態(tài)成員變量顯示賦值
    d) 執(zhí)行所有靜態(tài)代碼塊
    (c和d具體順序是按照代碼書寫的先后順序)
    等到靜態(tài)代碼塊都執(zhí)行完畢,類加載完成;
    對(duì)象的創(chuàng)建過(guò)程
    JVM遇到new關(guān)鍵字,首先回去堆內(nèi)存中開(kāi)辟空間;
    為所有非靜態(tài)的成員變量分配空間,賦默認(rèn)值;
    調(diào)用相應(yīng)的構(gòu)造函數(shù)進(jìn)棧執(zhí)行;
    構(gòu)造函數(shù)執(zhí)行時(shí)首先要執(zhí)行隱式三步:
    a) super():調(diào)用父類構(gòu)造函數(shù) // 如果第一行是this就調(diào)用this
    b) 給對(duì)象中所有非靜態(tài)成員變量顯示賦值;
    c) 執(zhí)行構(gòu)造代碼塊;
    (b和c具體執(zhí)行順序,是按照代碼書寫的先后順序)
    隱式三步執(zhí)行結(jié)束后,開(kāi)始執(zhí)行構(gòu)造函數(shù)中的代碼;
    等到構(gòu)造函數(shù)結(jié)束出棧,對(duì)象才算創(chuàng)建完成
向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI