之前整理了《JVM之類(lèi)加載機(jī)制》的文章焦人,對(duì)于一個(gè)類(lèi)的初始化階段介紹太過(guò)簡(jiǎn)略奶甘,這里再開(kāi)一篇文章篷店,著重介紹類(lèi)的初始化流程。
類(lèi)初始化是類(lèi)加載過(guò)程的最后一個(gè)階段臭家,到初始化階段疲陕,才真正開(kāi)始執(zhí)行類(lèi)中的Java程序代碼。虛擬機(jī)規(guī)范嚴(yán)格規(guī)定了有且只有四種情況必須立即對(duì)類(lèi)進(jìn)行初始化:
遇到new钉赁、getstatic蹄殃、putstatic、invokestatic這四條字節(jié)碼指令時(shí)橄霉,如果類(lèi)還沒(méi)有進(jìn)行過(guò)初始化窃爷,則需要先觸發(fā)其初始化。生成這四條指令最常見(jiàn)的Java代碼場(chǎng)景是:使用new關(guān)鍵字實(shí)例化對(duì)象時(shí)姓蜂、讀取或設(shè)置一個(gè)類(lèi)的靜態(tài)字段(static)時(shí)(被static修飾又被final修飾的按厘,已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)、以及調(diào)用一個(gè)類(lèi)的靜態(tài)方法時(shí)钱慢。
使用Java.lang.refect包的方法對(duì)類(lèi)進(jìn)行反射調(diào)用時(shí)导狡,如果類(lèi)還沒(méi)有進(jìn)行過(guò)初始化伍派,則需要先觸發(fā)其初始化。
當(dāng)初始化一個(gè)類(lèi)的時(shí)候,如果發(fā)現(xiàn)其父類(lèi)還沒(méi)有進(jìn)行初始化贞滨,則需要先觸發(fā)其父類(lèi)的初始化糖赔。
當(dāng)虛擬機(jī)啟動(dòng)時(shí)窿祥,用戶(hù)需要指定一個(gè)要執(zhí)行的主類(lèi)顶燕,虛擬機(jī)會(huì)先執(zhí)行該主類(lèi),也就是main方法所在的類(lèi)饿敲。
上面所說(shuō)時(shí)顯示的對(duì)象創(chuàng)建妻导,還有幾種隱式初始化的方式也說(shuō)明一下:
給String類(lèi)型變量賦值時(shí),若String對(duì)象在常量池中不存在怀各,則創(chuàng)建一個(gè)新的String對(duì)象
對(duì)String對(duì)象進(jìn)行拼接操作倔韭,同上
自動(dòng)裝箱機(jī)制可能會(huì)引起一個(gè)原子類(lèi)型的包裝類(lèi)對(duì)象被創(chuàng)建。
這里再說(shuō)一下類(lèi)不被初始化的情況:
對(duì)于靜態(tài)字段(沒(méi)有final修飾)瓢对,只有直接定義這個(gè)字段的類(lèi)才會(huì)被初始化寿酌,子類(lèi)調(diào)用父類(lèi)的靜態(tài)字段并不會(huì)觸發(fā)子類(lèi)的初始化
static final 修飾的常量,在編輯時(shí)就存入了調(diào)用者的Class文件常量池中硕蛹,調(diào)用時(shí)并不會(huì)觸發(fā)定義類(lèi)的初始化醇疼,也就是這個(gè)常量已經(jīng)使用的類(lèi)綁定硕并。
數(shù)組初始化過(guò)程并不會(huì)觸發(fā)引用類(lèi)的初始化
類(lèi)或接口初始化(準(zhǔn)備階段)
編譯器自動(dòng)收集類(lèi)變量賦值以及靜態(tài)代碼塊后自動(dòng)合并生成類(lèi)的<clint>(),類(lèi)開(kāi)始初始化時(shí)會(huì)為static變量賦上零值僵腺。
- <clint>()對(duì)于類(lèi)和接口來(lái)說(shuō)這個(gè)方法并不是必須的鲤孵。
- <clint>()中,靜態(tài)語(yǔ)句只能訪問(wèn)定義在它之前定義的靜態(tài)變量辰如,定義在它之后的靜態(tài)變量,可以賦值贵试,但不能訪問(wèn)琉兜。
- 子類(lèi)<clint>()不需要顯示的調(diào)用父類(lèi)的構(gòu)造器,JVM保證子類(lèi)的<clint>()執(zhí)行之前毙玻,父類(lèi)的<clint>()已經(jīng)執(zhí)行完畢豌蟋。
- 由于父類(lèi)的<clint>()先執(zhí)行,所以父類(lèi)的靜態(tài)語(yǔ)句優(yōu)先與子類(lèi)的靜態(tài)語(yǔ)句執(zhí)行
- 先對(duì)類(lèi)桑滩,接口的執(zhí)行<clint>()時(shí)并不需要執(zhí)行父接口的<clint>()方法梧疲,只有使用父接口定義的變量時(shí),父接口才會(huì)初始化运准。接口的實(shí)現(xiàn)類(lèi)初始化時(shí)也不會(huì)調(diào)用接口的<clint>()
- JVM保證一個(gè)類(lèi)的<clint>()執(zhí)行時(shí)線程安全的幌氮,多線程執(zhí)行類(lèi)的<clint>()時(shí)只能有一個(gè)被執(zhí)行,其余線程等待(執(zhí)行完畢后其他線程不再進(jìn)入<clint>())胁澳。如果一個(gè)類(lèi)的<clint>()執(zhí)行耗時(shí)操作该互,可能會(huì)造成多進(jìn)程阻塞
這里還有幾個(gè)注意點(diǎn):
接口也有初始化過(guò)程,在接口中不能使用“static{}”語(yǔ)句塊韭畸,但編譯器仍然會(huì)為接口生成<clint>()宇智,用于初始化接口中定義的成員變量(實(shí)際上是static final修飾的全局常量)。
二者在初始化時(shí)最主要的區(qū)別是:當(dāng)一個(gè)類(lèi)在初始化時(shí)胰丁,要求其父類(lèi)全部已經(jīng)初始化過(guò)了随橘,但是一個(gè)接口在初始化時(shí),并不要求其父接口全部都完成了初始化锦庸,只有在真正使用到父接口的時(shí)候(如引用接口中定義的常量)机蔗,才會(huì)初始化該父接口。這點(diǎn)也與類(lèi)初始化的情況很不同酸员,調(diào)用類(lèi)中的static final常量時(shí)并不會(huì) 觸發(fā)該類(lèi)的初始化蜒车,但是調(diào)用接口中的static final常量時(shí)便會(huì)觸發(fā)該接口的初始化
類(lèi)變量的賦值
下面簡(jiǎn)要說(shuō)明下final、static幔嗦、static final修飾的字段賦值的區(qū)別:
static修飾的字段在類(lèi)加載過(guò)程中的準(zhǔn)備階段被初始化為0或null等默認(rèn)值酿愧,而后在初始化階段(觸發(fā)類(lèi)構(gòu)造器<clint>())才會(huì)被賦予代碼中設(shè)定的值,如果沒(méi)有設(shè)定值邀泉,那么它的值就為默認(rèn)值嬉挡。
final修飾的字段在運(yùn)行時(shí)被初始化(可以直接賦值钝鸽,也可以在實(shí)例構(gòu)造器中賦值),一旦賦值便不可更改庞钢;
static final修飾的字段在Javac時(shí)生成ConstantValue屬性拔恰,在類(lèi)加載的準(zhǔn)備階段根據(jù)ConstantValue的值為該字段賦值,它沒(méi)有默認(rèn)值基括,必須顯式地賦值颜懊,否則Javac時(shí)會(huì)報(bào)錯(cuò)》缑螅可以理解為在編譯期即把結(jié)果放入了常量池中河爹。
實(shí)例對(duì)象的初始化(初始化階段)
編譯器自動(dòng)收集實(shí)例變量初始化以及實(shí)例代碼塊后自動(dòng)合并生成類(lèi)的<init>()
- 子類(lèi)初始化時(shí)會(huì)先調(diào)用父類(lèi)<init>(),用以保證子類(lèi)能正常初始化桐款。
- 執(zhí)行子類(lèi)的<init>()
那么從上面可以知道咸这,一個(gè)類(lèi)在初始化過(guò)程中,構(gòu)造方法的執(zhí)行過(guò)程如下:
- 父類(lèi)的<clint>()
- 子類(lèi)的<clint>()
- 父類(lèi)的<init>()
- 子類(lèi)的<init>()
一個(gè)子類(lèi)自身代碼的初始化執(zhí)行順序如下:
Static Field Initial (類(lèi)變量)
Static Patch Initial (靜態(tài)初始化塊)
Field Initial (成員變量)
Field Patch Initial (初始化塊)
Structure Initial (構(gòu)造器)
上面第一條和第二條依據(jù)代碼定義的順序不同魔眨,執(zhí)行的順序也不同(定義在靜態(tài)代碼塊之后的的類(lèi)變量可以被靜態(tài)代碼塊賦值媳维,但是不能被訪問(wèn))
舉個(gè)栗子驗(yàn)證一下
public class ClassInitTest {
public static void main(String[] args) {
new Child();
}
public static class Parent {
static {
System.out.println("Parent static code");
}
public Parent() {
System.out.println("Parent constructor code");
}
}
public static class Child extends Parent {
private int mInt = 100;
{
System.out.println(mInt);
mInt = 200;
System.out.println(mInt);
}
static {
System.out.println("Child static code");
}
public Child() {
System.out.println("Child constructor code");
System.out.println(mInt);
}
}
}
打印如下:
Parent static code
Child static code
Parent constructor code
100
200
Child constructor code
200
上面可以看到即使類(lèi)變量定義在成語(yǔ)變量和初始代碼塊之下也是先被執(zhí)行的,同時(shí)我們可以看到構(gòu)造器的代碼時(shí)最后執(zhí)行的遏暴。