引入
?在上一篇中,我們已經(jīng)了解到類(lèi)是如何加載到內(nèi)存中去的。上一篇中曾聊到過(guò)這樣的一個(gè)問(wèn)題:類(lèi)加載和對(duì)象實(shí)例化有什么區(qū)別和聯(lián)系?
?在上一篇中,我是這樣來(lái)描述的:
- 如果對(duì)Java有一些了解,你就應(yīng)該知道類(lèi)可以分為兩個(gè)大的部分:類(lèi)本身和類(lèi)的實(shí)例。類(lèi)加載機(jī)制加載的是類(lèi)本身,而類(lèi)的實(shí)例化是針對(duì)的類(lèi)的實(shí)例。
- 一個(gè)類(lèi)只會(huì)被加載一次,所以在方法區(qū)中只有一個(gè)java.lang.Class對(duì)象能代表當(dāng)前類(lèi);而一個(gè)類(lèi)的實(shí)例可以有多個(gè),實(shí)例對(duì)象基本上都是在堆中分配的內(nèi)存。
- 類(lèi)的實(shí)例化必須依賴(lài)于類(lèi)加載,在實(shí)例化時(shí)必須是被jvm認(rèn)可的類(lèi)型,而被jvm認(rèn)可就是類(lèi)的加載。類(lèi)的實(shí)例化干的第一件事就是檢查當(dāng)前類(lèi)是不是已經(jīng)被加載過(guò)了,如果沒(méi)有加載,則應(yīng)先加載類(lèi)。
?這一篇我們就來(lái)看看類(lèi)在實(shí)例化的時(shí)候到底干了些什么。相信通過(guò)這部分內(nèi)容,能對(duì)對(duì)象的實(shí)例化有一個(gè)初步的認(rèn)識(shí),而通過(guò)這兩篇的內(nèi)容能對(duì)類(lèi)在虛擬機(jī)中是如何被使用的有一個(gè)比較全面的認(rèn)知。
?請(qǐng)注意,本篇文章的內(nèi)容是基于最常用的HotSpot虛擬機(jī)和Java堆為例展開(kāi)的。
對(duì)象的創(chuàng)建過(guò)程
?眾所周知,Java是一門(mén)面向?qū)ο蟮恼Z(yǔ)言,在程序運(yùn)行期間,無(wú)時(shí)無(wú)刻都有對(duì)象被創(chuàng)建出來(lái)。我們都知道在應(yīng)用程序中可以通過(guò)new運(yùn)算符來(lái)創(chuàng)建新的對(duì)象,那么在虛擬機(jī)中又是如何運(yùn)轉(zhuǎn)的呢?如果你對(duì)對(duì)象創(chuàng)建有一些了解,可能會(huì)說(shuō):虛擬機(jī)會(huì)自動(dòng)調(diào)用構(gòu)造函數(shù)來(lái)產(chǎn)生對(duì)象。那么,虛擬機(jī)又是如何調(diào)用你的構(gòu)造函數(shù)的呢?一個(gè)對(duì)象真的是在構(gòu)造函數(shù)中被創(chuàng)建的嗎?
?注意,本篇內(nèi)容討論的對(duì)象并不包括數(shù)組和Class對(duì)象。
?總的來(lái)說(shuō),對(duì)象的創(chuàng)建應(yīng)該包含這樣的幾個(gè)大步驟:類(lèi)加載檢查、分配空間、初始化零值、設(shè)置對(duì)象頭和執(zhí)行構(gòu)造函數(shù)。下面我們將針對(duì)這些步驟一個(gè)一個(gè)的進(jìn)行分析,每一個(gè)步驟到底干了什么。
類(lèi)加載檢查
?簡(jiǎn)而言之,虛擬機(jī)如果遇到一條字節(jié)碼為new的指令時(shí),首先會(huì)去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類(lèi)的符號(hào)引用。
?并且檢查這個(gè)符號(hào)引用代表的類(lèi)是否已經(jīng)經(jīng)過(guò)類(lèi)加載階段(被加載、解析和初始化過(guò))。如果沒(méi)有,則應(yīng)先進(jìn)行類(lèi)加載,這是對(duì)象實(shí)例化的硬性前提。
?這一步很好理解,說(shuō)白了就是判斷我們需要實(shí)例化的類(lèi)是不是已經(jīng)被加載進(jìn)內(nèi)存了。在聊類(lèi)加載機(jī)制的時(shí)候,我們有提到這樣的內(nèi)容:每一個(gè)類(lèi)被加載后都會(huì)在方法區(qū)中創(chuàng)建一個(gè)java.lang.Class對(duì)象。這句話(huà)實(shí)際上是一個(gè)高度概括后的總結(jié),忽略了很多的細(xì)節(jié)。
?針對(duì)上面提到的細(xì)節(jié),這里補(bǔ)充一點(diǎn)內(nèi)容:虛擬機(jī)在執(zhí)行類(lèi)加載任務(wù)后,關(guān)于這個(gè)類(lèi)的定義最終會(huì)被放到方法區(qū)中的運(yùn)行時(shí)常量池中去。注意在這里我用了“最終”一詞,在類(lèi)加載的過(guò)程中,關(guān)于這個(gè)類(lèi)的定義是放在方法區(qū)的Class文件常量表中,在初始化完成后,虛擬機(jī)才會(huì)把這個(gè)類(lèi)的定義搬到方法區(qū)的運(yùn)行時(shí)常量池中。
分配空間
?在類(lèi)加載檢查通過(guò)后,虛擬機(jī)開(kāi)始為新生對(duì)象分配空間;
?事實(shí)上,對(duì)象所需內(nèi)存空間的大小在類(lèi)加載完成后便已經(jīng)確定了。
?分配空間很好理解,就是把一塊空閑的空間從堆空間中劃分出來(lái),就像切蛋糕一樣,把一塊切好的蛋糕分給你,你就可以吃這塊蛋糕了。
?分配空間有兩種方式:
- 指針碰撞:假設(shè)Java堆中的內(nèi)存是規(guī)整的,所有空閑的在一邊,所有被使用的在另一邊,中間有一個(gè)臨界指針,這個(gè)時(shí)候分配內(nèi)存就只需要把指針向著空閑的那邊移動(dòng)要分配空間大小的距離就可以了;
- 空閑列表:如果Java堆中的內(nèi)存并不規(guī)整,被用的和空閑的混雜在一起,就只能維護(hù)一個(gè)列表,列表上記錄下哪些內(nèi)存是空閑的,這個(gè)時(shí)候分配內(nèi)存就是在列表中找一塊足夠大的區(qū)域進(jìn)行分配,分配完后需要維護(hù)列表上的記錄;
?這兩種分配方式各有優(yōu)勢(shì)和缺點(diǎn),虛擬機(jī)會(huì)根據(jù)不同的垃圾回收機(jī)制采用相應(yīng)的分配方式。具體的內(nèi)容在虛擬機(jī)垃圾回收機(jī)制進(jìn)行詳細(xì)分析。
?關(guān)于虛擬機(jī)GC參見(jiàn):此處應(yīng)該插眼。
初始化零值
?在內(nèi)存分配之后,虛擬機(jī)會(huì)把部分內(nèi)存空間進(jìn)行初始化為零值。
?注意"部分"一詞,這里的部分指的是除對(duì)象頭以外的空間。接下來(lái)會(huì)談到這一塊,先有這么個(gè)概念就行。
?如果你還有印象的話(huà),我們?cè)陬?lèi)加載的準(zhǔn)備階段也提到了給剛分配的內(nèi)存初始化對(duì)應(yīng)類(lèi)型的零值。在這里,其實(shí)也是一樣的,主要作用是為了保證對(duì)象的實(shí)例字段(在類(lèi)加載的準(zhǔn)備階段則是為了保證類(lèi)變量)可以不用賦初始值就直接使用,這種情況下訪問(wèn)到這些字段的值就是對(duì)應(yīng)數(shù)據(jù)類(lèi)型的零值。
設(shè)置對(duì)象頭
?給對(duì)象的分配的空間中,有一部分是對(duì)象頭(Header),這一部分存儲(chǔ)了對(duì)象的相關(guān)信息,但不是對(duì)象的實(shí)例數(shù)據(jù);
?Header中主要包括有:對(duì)象的所屬類(lèi)、對(duì)象的hashcode、GC的分代年齡信息等等。
?這里你應(yīng)該知道為什么初始化零值的時(shí)候?yàn)槭裁匆懦龑?duì)象頭的空間,因?yàn)閷?duì)象頭的初始化有單獨(dú)的豪華包間,不需要在上一步初始化。
?設(shè)置對(duì)象頭這部分內(nèi)容不理解沒(méi)關(guān)系,等到我們分析了對(duì)象的內(nèi)存布局再回過(guò)頭來(lái)看這塊就明白了。
執(zhí)行<init>()方法
?一個(gè)對(duì)象的對(duì)象頭被初始化后,虛擬機(jī)就算干完了他應(yīng)該的事情:創(chuàng)建了一個(gè)對(duì)象,分配了空間,實(shí)例數(shù)據(jù)也有了初始值。但從我們的視角來(lái)看,他并沒(méi)有,因?yàn)橛晌覀兎峙涞娜蝿?wù)還沒(méi)有干呀!
?所以,還有最后的一個(gè)步驟,按照我們的意愿對(duì)新生對(duì)象進(jìn)行初始化。
?執(zhí)行<init>()方法:包含非靜態(tài)成員變量的賦值、非靜態(tài)代碼塊、構(gòu)造函數(shù)三塊。
?<init>()方法并不等同于我們的構(gòu)造函數(shù),它包含了我們對(duì)非靜態(tài)成員變量(以下簡(jiǎn)稱(chēng)實(shí)例變量)的賦值、非靜態(tài)代碼塊、構(gòu)造函數(shù)幾者的結(jié)合。
?這三塊內(nèi)容的執(zhí)行順序?yàn)椋?/p>
- 非成員變量初始化
- 非靜態(tài)代碼塊
- 構(gòu)造函數(shù)
?并且和類(lèi)加載過(guò)程相似,如果一個(gè)類(lèi)繼承自另一個(gè)類(lèi),那么在實(shí)例化這個(gè)類(lèi)時(shí)會(huì)先執(zhí)行父類(lèi)的<init>()方法。如下的代碼片段所示:
// 父類(lèi)
public class Parent {
public int parentValue = 10;
{
System.out.println("Parent類(lèi)的代碼塊開(kāi)始...");
System.out.println("Parent類(lèi)實(shí)例的parentValue第一次 = " + parentValue);
parentValue = 20;
System.out.println("Parent類(lèi)實(shí)例的parentValue第二次 = " + parentValue);
System.out.println("Parent類(lèi)的代碼塊結(jié)束...");
}
public Parent(){
System.out.println("Parent類(lèi)的構(gòu)造方法...");
}
}
// 子類(lèi)
public class Sub extends Parent {
public int value = parentValue;
{
System.out.println("Sub類(lèi)的代碼塊開(kāi)始...");
System.out.println("Sub類(lèi)實(shí)例的value第一次 = " + value);
System.out.println("Sub類(lèi)的代碼塊結(jié)束...");
}
public Sub(){
System.out.println("Sub類(lèi)的構(gòu)造方法...");
}
}
// 入口
public class ClinitTest {
public static void main(String[] args){
Sub sub = new Sub();
}
}
/*******************結(jié)果********************/
Parent類(lèi)的代碼塊開(kāi)始...
Parent類(lèi)實(shí)例的parentValue第一次 = 10
Parent類(lèi)實(shí)例的parentValue第二次 = 20
Parent類(lèi)的代碼塊結(jié)束...
Parent類(lèi)的構(gòu)造方法...
Sub類(lèi)的代碼塊開(kāi)始...
Sub類(lèi)實(shí)例的value第一次 = 20
Sub類(lèi)的代碼塊結(jié)束...
Sub類(lèi)的構(gòu)造方法...
/*******************解析********************/
// 在main方法中,我們new了一個(gè)Sub類(lèi),所以肯定會(huì)去實(shí)例化Sub類(lèi)
// Sub類(lèi)繼承自Parent類(lèi),所以在實(shí)例化Sub之前會(huì)先實(shí)例化Parent類(lèi)
// 關(guān)于執(zhí)行順序,可自行驗(yàn)證
?在上一篇中我們也提到了類(lèi)構(gòu)造器<clinit>()方法和構(gòu)造方法<init>()的區(qū)別,這里再簡(jiǎn)單做一下比較。
- 這兩個(gè)方法都是在編譯階段生成的,即在.class文件中有對(duì)應(yīng)的字節(jié)碼描述;但用途不一樣,clinit方法用于初始化類(lèi)變量,而init方法用于初始化實(shí)例變量、執(zhí)行非靜態(tài)代碼塊和執(zhí)行構(gòu)造函數(shù);
- <clinit>()方法一定比<init>()方法先開(kāi)始執(zhí)行;
?對(duì)于<clinit>()方法一定比<init>()方法先開(kāi)始執(zhí)行,這個(gè)只是表示類(lèi)的初始化一定會(huì)比實(shí)例的初始化先開(kāi)始。但這并不能表明一定是<clinit>()方法執(zhí)行完畢后才會(huì)執(zhí)行<init>()。不信你看下面的代碼片段:
public class Com {
// 類(lèi)變量
public static int A = 6;
static {
System.out.println("靜態(tài)代碼塊開(kāi)始");
System.out.println("靜態(tài)代碼塊中A = " + A);
Com com = new Com();
System.out.println("靜態(tài)代碼塊結(jié)束");
}
// 實(shí)例變量
public int B = 5;
{
System.out.println("非靜態(tài)代碼塊開(kāi)始");
System.out.println("非靜態(tài)代碼塊中B = " + B);
System.out.println("非靜態(tài)代碼塊結(jié)束");
}
public Com() {
System.out.println("構(gòu)造函數(shù)開(kāi)始");
System.out.println("構(gòu)造函數(shù)結(jié)束");
}
}
public class ClinitTest {
public static void main(String[] args){
System.out.println("main方法中 Com.A = " + Com.A);
}
}
// 輸出:
// 靜態(tài)代碼塊開(kāi)始
// 靜態(tài)代碼塊中A = 6
// 非靜態(tài)代碼塊開(kāi)始
// 非靜態(tài)代碼塊中B = 5
// 非靜態(tài)代碼塊結(jié)束
// 構(gòu)造函數(shù)開(kāi)始
// 構(gòu)造函數(shù)結(jié)束
// 靜態(tài)代碼塊結(jié)束
// main方法中 Com.A = 6
對(duì)象的內(nèi)存布局
?在HotSpot虛擬機(jī)中,對(duì)象在堆內(nèi)存中的存儲(chǔ)布局可以按邏輯劃分為三塊:對(duì)象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)和對(duì)齊填充(Padding)。
對(duì)象頭(Header區(qū)域)
?這部分是重頭戲,要把所有點(diǎn)都分析透徹其實(shí)相當(dāng)繁瑣。本篇文章也只會(huì)盡可能的說(shuō)一些常見(jiàn)的點(diǎn),不會(huì)在這里面下很多的功夫。
?總的來(lái)說(shuō),對(duì)象頭里面可以分為三個(gè)部分:Mark Word、類(lèi)型指針、數(shù)組長(zhǎng)度。分別簡(jiǎn)單認(rèn)識(shí)下。
Mark Word
?Mark Word在32位和64位的虛擬機(jī)中分別占用32個(gè)bit和64個(gè)bit(本章只討論32位,64位的情況暫不討論),用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù)。比如hashCode、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向鎖的線程ID、對(duì)象分代年齡等。
類(lèi)型指針
?類(lèi)型指針,32位,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定該對(duì)象是哪個(gè)類(lèi)的實(shí)例。
數(shù)據(jù)長(zhǎng)度
?簡(jiǎn)單說(shuō),就是虛擬機(jī)有可能(當(dāng)數(shù)組的長(zhǎng)度不確定時(shí))無(wú)法從數(shù)組的元數(shù)據(jù)中獲取到數(shù)組的大小,所以數(shù)組長(zhǎng)度要存放在對(duì)象頭中。數(shù)組長(zhǎng)度占用4個(gè)字節(jié)(32)位。
?下面分情況說(shuō)明在32位的虛擬機(jī)中對(duì)象頭的大?。?/p>
?如果對(duì)象不是Java數(shù)組,那就不需要數(shù)組長(zhǎng)度區(qū)域,所以對(duì)象頭是Mark Word + 類(lèi)型指針 = 32bit + 32bit = 64bit(8個(gè)字節(jié))
?如果對(duì)象是Java數(shù)組,那就需要數(shù)組長(zhǎng)度區(qū)域,所以對(duì)象頭是Mark Word + 類(lèi)型指針 + 數(shù)組長(zhǎng)度 = 32bit + 32bit + 32bit = 96bit(12個(gè)字節(jié))
?重點(diǎn)關(guān)注下MarkWord區(qū)域,在對(duì)象未被同步鎖鎖定的情況下,25個(gè)bit放hashCode,4bit放該對(duì)象的分代年齡,2bit用于存儲(chǔ)鎖的標(biāo)志位(00:輕量級(jí)鎖/自旋鎖;01:未被鎖或者偏向鎖;10:重量級(jí)鎖;11:GC標(biāo)記),1bit用于存儲(chǔ)鎖是否啟用偏向鎖(0:否;1:是)。如下表所示:
| 25位 | 4位 | 1位 | 2位 |
|---|---|---|---|
| 對(duì)象的hashCode | 對(duì)象的分代年齡 | 是否啟用偏向鎖:0 | 鎖標(biāo)志位:01 |
?對(duì)于各種鎖狀態(tài)下的對(duì)象內(nèi)存布局,將在鎖升級(jí)的時(shí)候詳細(xì)說(shuō)明。畢竟放到這里來(lái)理解實(shí)在太繁瑣且空洞,容易丟了西瓜撿芝麻。
?關(guān)于鎖的升級(jí)過(guò)程參見(jiàn):此處應(yīng)該插眼。
(一)CMS垃圾回收器真的可以增大GC分代年齡嗎?
?答:不能。CMS垃圾回收器的默認(rèn)GC分代年齡是15,根據(jù)對(duì)象的內(nèi)存布局來(lái)看,Mark Word區(qū)域中給對(duì)象記錄分代年齡的空間只有4bit,而4bit所能表示的最大值為"1111",15已經(jīng)是最大值了。
(二)說(shuō)說(shuō)成員變量和局部變量的區(qū)別?
- 成員變量可以被public,private,static等修飾符所修飾,而局部變量不能被訪問(wèn)控制修飾符及static所修飾;但是,成員變量和局部變量都能被final所修飾;
- 如果成員變量是使用static修飾的,那么這個(gè)成員變量是屬于類(lèi)的(類(lèi)變量),如果沒(méi)有使用static修飾,這個(gè)成員變量是屬于實(shí)例的(實(shí)例變量)。類(lèi)信息存在于方法區(qū),對(duì)象存在于堆內(nèi)存,局部變量則存在于棧內(nèi)存。
- 靜態(tài)成員變量是類(lèi)的一部分,隨著類(lèi)的卸載而消亡;實(shí)例成員變量是對(duì)象的一部分,它隨著對(duì)象的回收而消亡,而局部變量隨著方法的調(diào)用的結(jié)束而自動(dòng)消失。
- 成員變量如果沒(méi)有被賦初值,則會(huì)自動(dòng)以類(lèi)型的默認(rèn)值而賦值(一種情況例外:被 final 修飾的成員變量也必須顯式地賦值),而局部變量則不會(huì)自動(dòng)賦值。
擴(kuò)展區(qū)域
擴(kuò)展區(qū)域主體
這是一個(gè)沒(méi)有實(shí)現(xiàn)的擴(kuò)展。
上一篇:聊一聊虛擬機(jī)類(lèi)加載機(jī)制吧
上一篇:Java基礎(chǔ)之你肯定用過(guò)的三個(gè)關(guān)鍵字static、super和this