引言最近有位細(xì)心的朋友在閱讀筆者的文章時,,對java類的生命周期問題有一些疑惑,,筆者打開百度搜了一下相關(guān)的問題,看到網(wǎng)上的資料很少有把這個問題講明白的,,主要是因?yàn)槟壳皣鴥?nèi)java方面的教材大多只是告訴你“怎樣做”,,但至于“為什么這樣做”卻不多說,,所以造成大家在基礎(chǔ)和原理方面的知識比較匱乏,所以筆者今天就斗膽來講一下這個問題,,權(quán)當(dāng)拋磚引玉,,希望對在這個問題上有疑惑的朋友有所幫助,文中有說的不對的地方,,也希望各路高手前來指正,。 首先來了解一下jvm(java虛擬機(jī))中的幾個比較重要的內(nèi)存區(qū)域,這幾個區(qū)域在java類的生命周期中扮演著比較重要的角色:
除了以上四個內(nèi)存區(qū)域之外,jvm中的運(yùn)行時內(nèi)存區(qū)域還包括本地方法棧和程序計數(shù)器,這兩個區(qū)域與java類的生命周期關(guān)系不是很大,,在這里就不說了,,感興趣的朋友可以自己百度一下。 類的生命周期當(dāng)我們編寫一個java的源文件后,,經(jīng)過編譯會生成一個后綴名為class的文件,這種文件叫做字節(jié)碼文件,,只有這種字節(jié)碼文件才能夠在java虛擬機(jī)中運(yùn)行,,java類的生命周期就是指一個class文件從加載到卸載的全過程。 一個java類的完整的生命周期會經(jīng)歷加載,、連接,、初始化、使用,、和卸載五個階段,,當(dāng)然也有在加載或者連接之后沒有被初始化就直接被使用的情況,如圖所示: 下面我們就依次來說一說這五個階段,。 加載 在java中,,我們經(jīng)常會接觸到一個詞——類加載,它和這里的加載并不是一回事,,通常我們說類加載指的是類的生命周期中加載,、連接、初始化三個階段,。在加載階段,,java虛擬機(jī)會做什么工作呢?其實(shí)很簡單,,就是找到需要加載的類并把類的信息加載到j(luò)vm的方法區(qū)中,,然后在堆區(qū)中實(shí)例化一個java.lang.Class對象,作為方法區(qū)中這個類的信息的入口,。 類的加載方式比較靈活,,我們最常用的加載方式有兩種,一種是根據(jù)類的全路徑名找到相應(yīng)的class文件,,然后從class文件中讀取文件內(nèi)容,;另一種是從jar文件中讀取。另外,,還有下面幾種方式也比較常用:
對于加載的時機(jī),各個虛擬機(jī)的做法并不一樣,,但是有一個原則,,就是當(dāng)jvm“預(yù)期”到一個類將要被使用時,就會在使用它之前對這個類進(jìn)行加載,。比如說,,在一段代碼中出現(xiàn)了一個類的名字,jvm在執(zhí)行這段代碼之前并不能確定這個類是否會被使用到,,于是,,有些jvm會在執(zhí)行前就加載這個類,而有些則在真正需要用的時候才會去加載它,,這取決于具體的jvm實(shí)現(xiàn),。我們常用的hotspot虛擬機(jī)是采用的后者,就是說當(dāng)真正用到一個類的時候才對它進(jìn)行加載,。 加載階段是類的生命周期中的第一個階段,,加載階段之后,是連接階段,。有一點(diǎn)需要注意,,就是有時連接階段并不會等加載階段完全完成之后才開始,而是交叉進(jìn)行,,可能一個類只加載了一部分之后,,連接階段就已經(jīng)開始了。但是這兩個階段總的開始時間和完成時間總是固定的:加載階段總是在連接階段之前開始,,連接階段總是在加載階段完成之后完成,。 連接 連接階段比較復(fù)雜,一般會跟加載階段和初始化階段交叉進(jìn)行,,這個階段的主要任務(wù)就是做一些加載后的驗(yàn)證工作以及一些初始化前的準(zhǔn)備工作,,可以細(xì)分為三個步驟:驗(yàn)證、準(zhǔn)備和解析,。
連接階段完成之后會根據(jù)使用的情況(直接引用還是被動引用)來選擇是否對類進(jìn)行初始化,。 初始化 如果一個類被直接引用,,就會觸發(fā)類的初始化。在java中,,直接引用的情況有:
除了以上四種情況,其他使用類的方式叫做被動引用,,而被動引用不會觸發(fā)類的初始化,。請看主動引用的示例代碼: import java.lang.reflect.Field;import java.lang.reflect.Method;class InitClass{ static { System.out.println('初始化InitClass'); } public static String a = null; public static void method(){}}class SubInitClass extends InitClass{}public class Test1 { /** * 主動引用引起類的初始化的第四種情況就是運(yùn)行Test1的main方法時 * 導(dǎo)致Test1初始化,這一點(diǎn)很好理解,,就不特別演示了,。 * 本代碼演示了前三種情況,以下代碼都會引起InitClass的初始化,, * 但由于初始化只會進(jìn)行一次,,運(yùn)行時請將注解去掉,依次運(yùn)行查看結(jié)果,。 * @param args * @throws Exception */ public static void main(String[] args) throws Exception{ // 主動引用引起類的初始化一: new對象,、讀取或設(shè)置類的靜態(tài)變量、調(diào)用類的靜態(tài)方法,。 // new InitClass(); // InitClass.a = ''; // String a = InitClass.a; // InitClass.method(); // 主動引用引起類的初始化二:通過反射實(shí)例化對象,、讀取或設(shè)置類的靜態(tài)變量、調(diào)用類的靜態(tài)方法,。 // Class cls = InitClass.class; // cls.newInstance(); // Field f = cls.getDeclaredField('a'); // f.get(null); // f.set(null, 's'); // Method md = cls.getDeclaredMethod('method'); // md.invoke(null, null); // 主動引用引起類的初始化三:實(shí)例化子類,,引起父類初始化。 // new SubInitClass(); }} 上面的程序演示了主動引用觸發(fā)類的初始化的四種情況,。 類的初始化過程是這樣的:按照順序自上而下運(yùn)行類中的變量賦值語句和靜態(tài)語句,,如果有父類,,則首先按照順序運(yùn)行父類中的變量賦值語句和靜態(tài)語句。先看一個例子,,首先建兩個類用來顯示賦值操作: public class Field1{ public Field1(){ System.out.println('Field1構(gòu)造方法'); }}public class Field2{ public Field2(){ System.out.println('Field2構(gòu)造方法'); }} 下面是演示初始化順序的代碼: class InitClass2{ static{ System.out.println('運(yùn)行父類靜態(tài)代碼'); } public static Field1 f1 = new Field1(); public static Field1 f2; }class SubInitClass2 extends InitClass2{ static{ System.out.println('運(yùn)行子類靜態(tài)代碼'); } public static Field2 f2 = new Field2();}public class Test2 { public static void main(String[] args) throws ClassNotFoundException{ new SubInitClass2(); }} 上面的代碼中,,初始化的順序是:第03行,第05行,,第11行,,第13行。第04行是聲明操作,,沒有賦值,,所以不會被運(yùn)行。而下面的代碼: class InitClass2{ public static Field1 f1 = new Field1(); public static Field1 f2; static{ System.out.println('運(yùn)行父類靜態(tài)代碼'); }}class SubInitClass2 extends InitClass2{ public static Field2 f2 = new Field2(); static{ System.out.println('運(yùn)行子類靜態(tài)代碼'); }}public class Test2 { public static void main(String[] args) throws ClassNotFoundException{ new SubInitClass2(); }} 初始化順序?yàn)椋旱?2行,、第05行,、第10行、第12行,,各位可以運(yùn)行程序查看結(jié)果,。 在類的初始化階段,只會初始化與類相關(guān)的靜態(tài)賦值語句和靜態(tài)語句,,也就是有static關(guān)鍵字修飾的信息,,而沒有static修飾的賦值語句和執(zhí)行語句在實(shí)例化對象的時候才會運(yùn)行,。 使用 類的使用包括主動引用和被動引用,,主動引用在初始化的章節(jié)中已經(jīng)說過了,下面我們主要來說一下被動引用:
被動引用的示例代碼: class InitClass{ static { System.out.println('初始化InitClass'); } public static String a = null; public final static String b = 'b'; public static void method(){}}class SubInitClass extends InitClass{ static { System.out.println('初始化SubInitClass'); }}public class Test4 { public static void main(String[] args) throws Exception{ // String a = SubInitClass.a;// 引用父類的靜態(tài)字段,,只會引起父類初始化,而不會引起子類的初始化 // String b = InitClass.b;// 使用類的常量不會引起類的初始化 SubInitClass[] sc = new SubInitClass[10];// 定義類數(shù)組不會引起類的初始化 }} 最后總結(jié)一下使用階段:使用階段包括主動引用和被動引用,,主動飲用會引起類的初始化,,而被動引用不會引起類的初始化。 當(dāng)使用階段完成之后,,java類就進(jìn)入了卸載階段,。 卸載 關(guān)于類的卸載,筆者在單例模式討論篇:單例模式與垃圾回收一文中有過描述,,在類使用完之后,,如果滿足下面的情況,,類就會被卸載:
如果以上三個條件全部滿足,,jvm就會在方法區(qū)垃圾回收的時候?qū)︻愡M(jìn)行卸載,類的卸載過程其實(shí)就是在方法區(qū)中清空類信息,,java類的整個生命周期就結(jié)束了,。 總結(jié)做java的朋友對于對象的生命周期可能都比較熟悉,對象基本上都是在jvm的堆區(qū)中創(chuàng)建,,在創(chuàng)建對象之前,,會觸發(fā)類加載(加載、連接,、初始化),,當(dāng)類初始化完成后,根據(jù)類信息在堆區(qū)中實(shí)例化類對象,,初始化非靜態(tài)變量,、非靜態(tài)代碼以及默認(rèn)構(gòu)造方法,當(dāng)對象使用完之后會在合適的時候被jvm垃圾收集器回收,。讀完本文后我們知道,,對象的生命周期只是類的生命周期中使用階段的主動引用的一種情況(即實(shí)例化類對象)。而類的整個生命周期則要比對象的生命周期長的多,。 |
|