類的生命周期
其中怜校,加載,驗(yàn)證注竿,準(zhǔn)備茄茁,初始化和卸載這5個(gè)階段的順序是確定的,類的加載過程必須按照這種順序開始巩割,而類的解析不一定裙顽,類的解析可能在初始化階段之后再開始,這是為了支持Java語言的動態(tài)綁定
Java的動態(tài)綁定和靜態(tài)綁定
在Java中宣谈,當(dāng)你調(diào)用一個(gè)方法時(shí)愈犹,可能會在編譯時(shí)期(compile time)解析(resolve),也可能實(shí)在運(yùn)行時(shí)期(runtime)解析闻丑,這全取決于到底是一個(gè)靜態(tài)方法(static method)還是一個(gè)虛方法(virtual method)漩怎。如果是在編譯時(shí)期解析,那么就稱之為靜態(tài)綁定(static binding)嗦嗡,如果方法的調(diào)用是在運(yùn)行時(shí)期解析勋锤,那就是動態(tài)綁定(dynamic binding)或者延遲綁定(late binding)。Java是一門面向?qū)ο蟮木幊陶Z言酸钦,優(yōu)勢就在于支持多態(tài)(Polymorphism)怪得。多態(tài)使得父類型的引用變量可以引用子類型的對象。如果調(diào)用子類型對象的一個(gè)虛方法(非private,final or static)卑硫,編譯器將無法找到真正需要調(diào)用的方法,因?yàn)樗赡苁嵌x在父類型中的方法蚕断,也可能是在子類型中被重寫(override)的方法欢伏,這種情形,只能在運(yùn)行時(shí)進(jìn)行解析亿乳,因?yàn)橹挥性谶\(yùn)行時(shí)期硝拧,才能明確具體的對象到底是什么。這也是我們俗稱的運(yùn)行時(shí)或動態(tài)綁定(runtime or dynamic binding)葛假。另一方面障陶,private static和final方法將在編譯時(shí)解析,因?yàn)榫幾g器知道它們不能被重寫聊训,所有可能的方法都被定義在了一個(gè)類中抱究,這些方法只能通過此類的引用變量進(jìn)行調(diào)用。這叫做靜態(tài)綁定或編譯時(shí)綁定(static or compile time binding)带斑。所有的private,static和final方法都通過靜態(tài)綁定進(jìn)行解析鼓寺。這兩個(gè)概念的關(guān)系勋拟,與“方法重載”(overloading,靜態(tài)綁定)和“方法重寫”(overriding妈候,動態(tài)綁定)類似敢靡。動態(tài)綁定只有在重寫可能存在時(shí)才會用到,而重載的方法在編譯時(shí)期即可確定(這是因?yàn)樗鼈兛偸嵌x在同一個(gè)類里面)
總而言之苦银,其區(qū)別如下:
①靜態(tài)綁定在編譯時(shí)期啸胧,動態(tài)綁定在運(yùn)行時(shí)期。
②靜態(tài)綁定只用到類型信息幔虏,方法的解析根據(jù)引用變量的類型決定纺念,而動態(tài)綁定則根據(jù)實(shí)際引用的的對象決定
③在java中,private static 和 final 方法都是靜態(tài)綁定所计,只有虛方法才是動態(tài)綁定
④多態(tài)是通過動態(tài)綁定實(shí)現(xiàn)的柠辞。
類在何時(shí)會被初始化
- 遇到new(創(chuàng)建對象), getstatic(讀取靜態(tài)字段)主胧,putstatic(設(shè)置靜態(tài)字段)叭首,invokestatic(執(zhí)行靜態(tài)方法)的指令的時(shí)候,類才會被初始化
- 使用Java類中的反射踪栋,對類進(jìn)行反射調(diào)用
- 當(dāng)初始化子類的時(shí)候發(fā)現(xiàn)父類還沒被初始化焙格,這時(shí)候會先初始化父類,然后再初始化子類
- JVM啟動的時(shí)候Java程序的入口main方法所在的類
- Java的動態(tài)語言支持:MethodHandler中有涉及到static字段的讀取或者設(shè)置夷都,static方法的調(diào)用眷唉,這些也需要類進(jìn)行初始化,JDK1.7中囤官,Java的方法也能過當(dāng)作方法的參數(shù)進(jìn)行傳遞冬阳,類似于c語言中的函數(shù)指針
- 只有以上這幾種情況才可能主動觸發(fā)類的初始化,其他的情況党饮,類的初始化都是被動的
例如:
public class SubClass extends SuperClass {
static {
System.out.println("I am sub class!");
}
}
public class SuperClass {
public static int value = 9;
static {
System.out.println("I am super class!");
}
}
public class DemoMain {
public static void main(String[] args) {
System.out.println("test:" + SubClass.value);
}
}
SuperClass中定義了一個(gè)static字段value肝陪,在程序運(yùn)行的時(shí)候通過子類區(qū)調(diào)用value,這時(shí)候只會對SuperClass進(jìn)行初始化刑顺,而不會對SubClass初始化氯窍,運(yùn)行結(jié)果如下:
I am super class!
test:9
所以對于靜態(tài)字段的引用,雖然是SubClass.value蹲堂,但是由于value是static的狼讨,并且是在SuperClass中聲明的,所以只會初始化SuperClass柒竞,當(dāng)使用SubClass.value來訪問value的時(shí)候政供,這時(shí)候是跟SubClass無關(guān)的,父類中的static字段或者static方法是可以被子類覆蓋的,如果子類中有相同的聲明鲫骗,那么對于這個(gè)子類就會默認(rèn)覆蓋掉父類中對應(yīng)的聲明犬耻,如果子類中沒有相同的聲明,那么就直接繼承自父類中的相關(guān)方法或者變量执泰,但是static方法不具有多態(tài)枕磁,因?yàn)槎鄳B(tài)是對于一個(gè)對象來說才有多種形態(tài),對于類來說沒有多態(tài)的概念术吝,而static方法就是相對于類而言的计济,因?yàn)閟tatic方法不具備多態(tài)特性,舉個(gè)例子:
A是父類排苍,B繼承自A
A b = new B();
b.callMethod();
這就是b對象的多態(tài)沦寂,A中聲明了callMethod方法,B中也可能也聲明了callMethod方法淘衙,那么在b對象調(diào)用callMethod方法的時(shí)候執(zhí)行的是B對象中重寫的callMethod方法传藏,這是在運(yùn)行期間才知道callMethod方法要調(diào)用的是B的方法,多態(tài)就是Java中的動態(tài)綁定彤守,在運(yùn)行期間才能確定具體調(diào)用的是哪一個(gè)方法
類加載過程
- 通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制流毯侦,這句話的意思是:java文件經(jīng)過編譯后會生成class文件,class文件只是字節(jié)碼具垫,這個(gè)機(jī)器無法識別侈离,所以在類加載的時(shí)候需要先把class字節(jié)碼轉(zhuǎn)換成二進(jìn)制的格式,這樣機(jī)器才能識別
- 將前面由class字節(jié)碼轉(zhuǎn)換過來的二進(jìn)制字節(jié)流代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化成方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)筝蚕,這里我的理解是卦碾,一個(gè)類的信息(類全限定名,類中的方法起宽,字段等)是存儲在方法區(qū)中的洲胖,這里就是把二進(jìn)制字節(jié)流識別里面的一些類信息結(jié)構(gòu),然后存儲到指定的方法去(這里其實(shí)說的很模糊)
- 在內(nèi)存中生成一個(gè)Class對象來作為方法區(qū)這個(gè)類的訪問入口
在類的加載過程中坯沪,數(shù)組的加載不需要類加載器來加載宾濒,而是java虛擬機(jī)直接創(chuàng)建的,但是數(shù)據(jù)中的元素如果還是一個(gè)類對象屏箍,那么這個(gè)元素對應(yīng)的類也需要類加載器來加載;在生成Class對象的時(shí)候需要注意橘忱,Class對象其實(shí)是一個(gè)對象赴魁,但是它是存在方法區(qū)中的,正常情況下Class對象的引用存在JVM棧中钝诚,而對象實(shí)際分配的內(nèi)存空間是在堆內(nèi)存中
類的驗(yàn)證
- 類的驗(yàn)證是為了確保class文件的字節(jié)流中的信息符合當(dāng)前虛擬機(jī)的要求颖御,并不會導(dǎo)致虛擬機(jī)出現(xiàn)崩潰,如果驗(yàn)證失敗了那么JVM會拋出一個(gè)java.lang.VerifyError異常或者其子類潘拱,我曾經(jīng)就遇到過這么一個(gè)異常疹鳄,比如在Android4.0的系統(tǒng)上ReflectiveOperationException這個(gè)異常類,但是在Android6.0上就有這個(gè)類芦岂,那么在6.0下面編譯成功的apk放到4.0系統(tǒng)上運(yùn)行瘪弓,如果是如下代碼就會在apk安裝啟動的時(shí)候就會報(bào)這個(gè)VerifyError異常:
try {
// ...
} catch (ReflectiveOperationException e) {
}
但是如果修改成下面這種,在程序不運(yùn)行到這里的時(shí)候不會崩潰禽最,一切正常腺怯,一旦程序運(yùn)行到這里,那么就會報(bào)NoClassDefFoundError異常:
System.out.println("" + ReflectiveOperationException.class);
這個(gè)說明在程序運(yùn)行到這里的時(shí)候JVM才去加載ReflectiveOperationException這個(gè)類川无,但是加載失敗呛占,而前面一個(gè)是程序一啟動運(yùn)行的時(shí)候,JVM會去加載引用ReflectiveOperationException的這個(gè)類懦趋,并且加載成功了晾虑,然后就會對引用了ReflectiveOperationException的這個(gè)類進(jìn)行驗(yàn)證,說明JVM在解析try catch語句的時(shí)候是有所不同的
類驗(yàn)證主要做了以下幾種驗(yàn)證:
- 文件格式驗(yàn)證仅叫,例如魔數(shù)是否正確帜篇, 常量池中的常量是否有不被支持的常量類型(檢查常量tag標(biāo)志)等
- 元數(shù)據(jù),這個(gè)其實(shí)就是驗(yàn)證語法的合法性惑芭,例如abstract修飾的父類方法是否被子類實(shí)現(xiàn)等
- 字節(jié)碼驗(yàn)證坠狡,這個(gè)很復(fù)雜,暫時(shí)不了解遂跟,也不想了解
- 符號引用的驗(yàn)證逃沿,符號中的字符串描述的全限定名是否能找到對應(yīng)的類,類中是否能找到指定的方法字段幻锁,符號引用中的類凯亮,字段,方法的訪問性(private哄尔,protected假消,public)是否能夠被成功訪問,如果不行就會拋出NoSuchFieldError岭接,NoSuchMethodError富拗,IllegalAccessError
類的準(zhǔn)備階段
- 準(zhǔn)備階段是為類變量分配內(nèi)存并設(shè)置初始值,這些變量所使用的內(nèi)存都是在方法區(qū)中分配的鸣戴,但是要注意啃沪,準(zhǔn)備階段所分配內(nèi)存的變量都是被static修飾的,如果是實(shí)例變量或者其他局部變量窄锅,那會隨著對象的實(shí)例化在堆內(nèi)存中分配创千,假設(shè):
public static final int value = 9;
那么在類準(zhǔn)備階段后,由于value變量被static修飾,那么value的初始值會為0追驴,所以value就可以被其他類訪問械哟,當(dāng)類初始化之后,執(zhí)行了<clinit> ()方法殿雪,value的值才是9
類的解析
- 解析就是把class文件中的符號引用轉(zhuǎn)換成直接引用暇咆,符號引用就是類的全限定名,可以是任意的字面量冠摄,引用的目標(biāo)不一定已經(jīng)加載到內(nèi)存中糯崎,而直接引用就是直接指向目標(biāo)的指針,引用對象一定需要已經(jīng)被加載到內(nèi)存中河泳;Java中的多態(tài)(動態(tài)綁定)其實(shí)就是跟類的解析有關(guān)沃呢,類的解析可能發(fā)生在程序運(yùn)行期間(類初始化之后),因?yàn)閷τ诙鄳B(tài)來說在類的加載拆挥,驗(yàn)證薄霜,準(zhǔn)備過程中并不知道實(shí)際要調(diào)用哪一個(gè)對象的方法砚婆,只有在執(zhí)行代碼的時(shí)候才知道實(shí)際需要執(zhí)行哪一個(gè)對象的方法
類初始化
- 類初始化是類加載過程的最后一步了私植,初始化其實(shí)就是執(zhí)行構(gòu)造器的過程舷暮,構(gòu)造器是JVM自動生成的扒披,它是去自動搜集類的變量,靜態(tài)代碼塊中的語句合并產(chǎn)生的
- <clinit>()和類的構(gòu)造函數(shù)不同受裹,JVM會保證子類執(zhí)行<clinit>()方法之前會先執(zhí)行父類的<clinit>()方法宿百,不需要想構(gòu)造函數(shù)一樣需要顯式的調(diào)用父類的構(gòu)造函數(shù)壁公,所以O(shè)bject的<clinit>()一定是最先執(zhí)行的
- <clinit>()不是必須的洲拇,如果類中沒有對變量進(jìn)行賦值操作奈揍,也沒有靜態(tài)代碼塊,那么就沒有<clinit>()方法
- 虛擬機(jī)會保證<clinit>()方法在多線程的環(huán)境下同步執(zhí)行赋续,所以如果多線程同時(shí)去初始化一個(gè)類男翰,那么同一個(gè)時(shí)刻只有一個(gè)線程去執(zhí)行<clinit>()方法,其他線程都會等待纽乱,如果在一個(gè)類<clinit>()方法中有耗時(shí)人物蛾绎,可能造成多線程阻塞,例如在靜態(tài)代碼塊中執(zhí)行耗時(shí)操作
類加載器
- 類加載器(ClassLoader)注意一點(diǎn)鸦列,比較兩個(gè)類是否“相等”的前提必須是這兩個(gè)類必須是同一個(gè)類加載加載的才有意義租冠,如果A類是由ClassLoader1加載,同時(shí)A類也由ClassLoader2加載了一次薯嗤,那么ClassLoader1加載的A和ClassLoader2加載的A不屬于同一個(gè)類肺稀,雖然都來自同一個(gè)Class文件,但是由于是不同的類加載器加載的所以依然是兩個(gè)獨(dú)立的類
- 雙親委派模型应民,雙親委派模型的工作過程是如果一個(gè)類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個(gè)類,而是把這個(gè)請求委派給父類的加載器去完成诲锹,每一個(gè)層次的類加載器都是如此繁仁,因此所以類加載請求最終都會傳送到頂層的啟動類加載器中,只有當(dāng)父類加載器反饋無法完成這個(gè)加載請求的時(shí)候归园,子類加載器才會嘗試自己去加載黄虱,使用雙親委派模型來進(jìn)行類加載的一個(gè)好處就是確保類加載的唯一性,例如Object對象被啟動類加載器加載過了庸诱,那么只要是java.lang.Object這個(gè)類全限定名來加載的都會去頂層的類加載器中找到已經(jīng)加載成功的Object對象捻浦,確保了Object的唯一性,避免不同的ClassLoader多次加載同一個(gè)類全限定名的類