引言
? ? ? ? 最近有位細(xì)心的朋友在閱讀筆者的文章時(shí)柒瓣,對(duì)java類的生命周期問題有一些疑惑吹截,筆者打開百度搜了一下相關(guān)的問題,看到網(wǎng)上的資料很少有把這個(gè)問題講明白的踪区,主要是因?yàn)槟壳皣鴥?nèi)java方面的教材大多只是告訴你“怎樣做”驱入,但至于“為什么這樣做”卻不多說赤炒,所以造成大家在基礎(chǔ)和原理方面的知識(shí)比較匱乏氯析,所以筆者今天就斗膽來講一下這個(gè)問題,權(quán)當(dāng)拋磚引玉莺褒,希望對(duì)在這個(gè)問題上有疑惑的朋友有所幫助掩缓,文中有說的不對(duì)的地方,也希望各路高手前來指正遵岩。
? ? 首先來了解一下jvm(java虛擬機(jī))中的幾個(gè)比較重要的內(nèi)存區(qū)域你辣,這幾個(gè)區(qū)域在java類的生命周期中扮演著比較重要的角色:
?方法區(qū):在java的虛擬機(jī)中有一塊專門用來存放已經(jīng)加載的類信息、常量尘执、靜態(tài)變量以及方法代碼的內(nèi)存區(qū)域舍哄,叫做方法區(qū)。
?常量池:常量池是方法區(qū)的一部分誊锭,主要用來存放常量和類中的符號(hào)引用等信息表悬。
? ? 堆區(qū):用于存放類的對(duì)象實(shí)例。
? ? 棧區(qū):也叫java虛擬機(jī)棧丧靡,是由一個(gè)一個(gè)的棧幀組成的后進(jìn)先出的棧式結(jié)構(gòu)蟆沫,棧楨中存放方法運(yùn)行時(shí)產(chǎn)生的局部變量、方法出口等信息温治。當(dāng)調(diào)用一個(gè)方法時(shí)饭庞,虛擬機(jī)棧中就會(huì)創(chuàng)建一個(gè)棧幀存放這些數(shù)據(jù),當(dāng)方法調(diào)用完成時(shí)熬荆,棧幀消失舟山,如果方法中調(diào)用了其他方法,則繼續(xù)在棧頂創(chuàng)建新的棧楨惶看。
? ? ? ? 除了以上四個(gè)內(nèi)存區(qū)域之外,jvm中的運(yùn)行時(shí)內(nèi)存區(qū)域還包括本地方法棧和程序計(jì)數(shù)器六孵,這兩個(gè)區(qū)域與java類的生命周期關(guān)系不是很大纬黎,在這里就不說了,感興趣的朋友可以自己百度一下劫窒。
類的生命周期
? ? ? ? 當(dāng)我們編寫一個(gè)java的源文件后本今,經(jīng)過編譯會(huì)生成一個(gè)后綴名為class的文件,這種文件叫做字節(jié)碼文件主巍,只有這種字節(jié)碼文件才能夠在java虛擬機(jī)中運(yùn)行冠息,java類的生命周期就是指一個(gè)class文件從加載到卸載的全過程。
? ? ? ? 一個(gè)java類的完整的生命周期會(huì)經(jīng)歷加載孕索、連接逛艰、初始化、使用搞旭、和卸載五個(gè)階段散怖,當(dāng)然也有在加載或者連接之后沒有被初始化就直接被使用的情況菇绵,如圖所示:
下面我們就依次來說一說這五個(gè)階段。
加載
? ? ? ?在java中镇眷,我們經(jīng)常會(huì)接觸到一個(gè)詞——類加載咬最,它和這里的加載并不是一回事,通常我們說類加載指的是類的生命周期中加載欠动、連接永乌、初始化三個(gè)階段。在加載階段具伍,java虛擬機(jī)會(huì)做什么工作呢翅雏?其實(shí)很簡單,就是找到需要加載的類并把類的信息加載到j(luò)vm的方法區(qū)中沿猜,然后在堆區(qū)中實(shí)例化一個(gè)java.lang.Class對(duì)象枚荣,作為方法區(qū)中這個(gè)類的信息的入口。
? ? ? ?類的加載方式比較靈活啼肩,我們最常用的加載方式有兩種橄妆,一種是根據(jù)類的全路徑名找到相應(yīng)的class文件,然后從class文件中讀取文件內(nèi)容祈坠;另一種是從jar文件中讀取害碾。另外,還有下面幾種方式也比較常用:
從網(wǎng)絡(luò)中獲壬饩小:比如10年前十分流行的Applet慌随。
根據(jù)一定的規(guī)則實(shí)時(shí)生成,比如設(shè)計(jì)模式中的動(dòng)態(tài)代理模式躺同,就是根據(jù)相應(yīng)的類自動(dòng)生成它的代理類阁猜。
從非class文件中獲取,其實(shí)這與直接從class文件中獲取的方式本質(zhì)上是一樣的蹋艺,這些非class文件在jvm中運(yùn)行之前會(huì)被轉(zhuǎn)換為可被jvm所識(shí)別的字節(jié)碼文件剃袍。
? ? ? ?對(duì)于加載的時(shí)機(jī),各個(gè)虛擬機(jī)的做法并不一樣捎谨,但是有一個(gè)原則民效,就是當(dāng)jvm“預(yù)期”到一個(gè)類將要被使用時(shí),就會(huì)在使用它之前對(duì)這個(gè)類進(jìn)行加載涛救。比如說畏邢,在一段代碼中出現(xiàn)了一個(gè)類的名字,jvm在執(zhí)行這段代碼之前并不能確定這個(gè)類是否會(huì)被使用到检吆,于是舒萎,有些jvm會(huì)在執(zhí)行前就加載這個(gè)類,而有些則在真正需要用的時(shí)候才會(huì)去加載它蹭沛,這取決于具體的jvm實(shí)現(xiàn)逆甜。我們常用的hotspot虛擬機(jī)是采用的后者虱肄,就是說當(dāng)真正用到一個(gè)類的時(shí)候才對(duì)它進(jìn)行加載。
? ? ? ?加載階段是類的生命周期中的第一個(gè)階段交煞,加載階段之后咏窿,是連接階段。有一點(diǎn)需要注意素征,就是有時(shí)連接階段并不會(huì)等加載階段完全完成之后才開始集嵌,而是交叉進(jìn)行,可能一個(gè)類只加載了一部分之后御毅,連接階段就已經(jīng)開始了根欧。但是這兩個(gè)階段總的開始時(shí)間和完成時(shí)間總是固定的:加載階段總是在連接階段之前開始,連接階段總是在加載階段完成之后完成端蛆。
連接
? ? ? ?連接階段比較復(fù)雜凤粗,一般會(huì)跟加載階段和初始化階段交叉進(jìn)行,這個(gè)階段的主要任務(wù)就是做一些加載后的驗(yàn)證工作以及一些初始化前的準(zhǔn)備工作今豆,可以細(xì)分為三個(gè)步驟:驗(yàn)證嫌拣、準(zhǔn)備和解析。
?驗(yàn)證:當(dāng)一個(gè)類被加載之后呆躲,必須要驗(yàn)證一下這個(gè)類是否合法异逐,比如這個(gè)類是不是符合字節(jié)碼的格式、變量與方法是不是有重復(fù)插掂、數(shù)據(jù)類型是不是有效灰瞻、繼承與實(shí)現(xiàn)是否合乎標(biāo)準(zhǔn)等等「ㄉ總之酝润,這個(gè)階段的目的就是保證加載的類是能夠被jvm所運(yùn)行。
?準(zhǔn)備:準(zhǔn)備階段的工作就是為類的靜態(tài)變量分配內(nèi)存并設(shè)為jvm默認(rèn)的初值璃弄,對(duì)于非靜態(tài)的變量要销,則不會(huì)為它們分配內(nèi)存。有一點(diǎn)需要注意谢揪,這時(shí)候蕉陋,靜態(tài)變量的初值為jvm默認(rèn)的初值捐凭,而不是我們?cè)诔绦蛑性O(shè)定的初值拨扶。jvm默認(rèn)的初值是這樣的:
基本類型(int、long茁肠、short患民、char、byte垦梆、boolean匹颤、float仅孩、double)的默認(rèn)值為0。
引用類型的默認(rèn)值為null印蓖。
常量的默認(rèn)值為我們程序中設(shè)定的值辽慕,比如我們?cè)诔绦蛑卸xfinal static int a = 100,則準(zhǔn)備階段中a的初值就是100赦肃。
??解析:這一階段的任務(wù)就是把常量池中的符號(hào)引用轉(zhuǎn)換為直接引用溅蛉。那么什么是符號(hào)引用,什么又是直接引用呢他宛?
????? 我們來舉個(gè)例子:我們要找一個(gè)人船侧,我們現(xiàn)有的信息是這個(gè)人的身份證號(hào)是1234567890。只有這個(gè)信息我們顯然找不到這個(gè)人厅各,但是通過公安局的身份系統(tǒng)镜撩,我們輸入1234567890這個(gè)號(hào)之后,就會(huì)得到它的全部信息:比如安徽省黃山市余暇村18號(hào)張三队塘,通過這個(gè)信息我們就能找到這個(gè)人了袁梗。這里,123456790就好比是一個(gè)符號(hào)引用人灼,而安徽省黃山市余暇村18號(hào)張三就是直接引用围段。
?????在內(nèi)存中也是一樣,比如我們要在內(nèi)存中找一個(gè)類里面的一個(gè)叫做show的方法投放,顯然是找不到奈泪。但是在解析階段,jvm就會(huì)把show這個(gè)名字轉(zhuǎn)換為指向方法區(qū)的的一塊內(nèi)存地址灸芳,比如c17164涝桅,通過c17164就可以找到show這個(gè)方法具體分配在內(nèi)存的哪一個(gè)區(qū)域了。這里show就是符號(hào)引用烙样,而c17164就是直接引用冯遂。在解析階段,jvm會(huì)將所有的類或接口名谒获、字段名蛤肌、方法名轉(zhuǎn)換為具體的內(nèi)存地址。
? ? ? ? 連接階段完成之后會(huì)根據(jù)使用的情況(直接引用還是被動(dòng)引用)來選擇是否對(duì)類進(jìn)行初始化批狱。
初始化
? ? ? ?如果一個(gè)類被直接引用裸准,就會(huì)觸發(fā)類的初始化。在java中赔硫,直接引用的情況有:
通過new關(guān)鍵字實(shí)例化對(duì)象炒俱、讀取或設(shè)置類的靜態(tài)變量、調(diào)用類的靜態(tài)方法。
通過反射方式執(zhí)行以上三種行為权悟。
初始化子類的時(shí)候砸王,會(huì)觸發(fā)父類的初始化。
作為程序入口直接運(yùn)行時(shí)(也就是直接調(diào)用main方法)峦阁。
? ? ? ? 除了以上四種情況谦铃,其他使用類的方式叫做被動(dòng)引用,而被動(dòng)引用不會(huì)觸發(fā)類的初始化榔昔。請(qǐng)看主動(dòng)引用的示例代碼:
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?{
/**?
* 主動(dòng)引用引起類的初始化的第四種情況就是運(yùn)行Test1的main方法時(shí)?
* 導(dǎo)致Test1初始化荷辕,這一點(diǎn)很好理解,就不特別演示了件豌。?
* 本代碼演示了前三種情況疮方,以下代碼都會(huì)引起InitClass的初始化,?
* 但由于初始化只會(huì)進(jìn)行一次茧彤,運(yùn)行時(shí)請(qǐng)將注解去掉骡显,依次運(yùn)行查看結(jié)果。?
*?@param?args?
*?@throws?Exception?
*/
public?static?void?main(String[] args)?throws?Exception{
// ?主動(dòng)引用引起類的初始化一: new對(duì)象曾掂、讀取或設(shè)置類的靜態(tài)變量惫谤、調(diào)用類的靜態(tài)方法。 ?
// ?new InitClass(); ?
// ?InitClass.a = ""; ?
// ?String a = InitClass.a; ?
// ?InitClass.method(); ?
// ?主動(dòng)引用引起類的初始化二:通過反射實(shí)例化對(duì)象珠洗、讀取或設(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); ?
// ?主動(dòng)引用引起類的初始化三:實(shí)例化子類许蓖,引起父類初始化蝴猪。 ?
// ?new SubInitClass(); ?
}
}
上面的程序演示了主動(dòng)引用觸發(fā)類的初始化的四種情況。
類的初始化過程是這樣的:按照順序自上而下運(yùn)行類中的變量賦值語句和靜態(tài)語句膊爪,如果有父類自阱,則首先按照順序運(yùn)行父類中的變量賦值語句和靜態(tài)語句。先看一個(gè)例子米酬,首先建兩個(gè)類用來顯示賦值操作:
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行是聲明操作跳芳,沒有賦值芍锦,所以不會(huì)被運(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é)果车胡。
在類的初始化階段,只會(huì)初始化與類相關(guān)的靜態(tài)賦值語句和靜態(tài)語句照瘾,也就是有static關(guān)鍵字修飾的信息匈棘,而沒有static修飾的賦值語句和執(zhí)行語句在實(shí)例化對(duì)象的時(shí)候才會(huì)運(yùn)行。
使用
?????? 類的使用包括主動(dòng)引用和被動(dòng)引用析命,主動(dòng)引用在初始化的章節(jié)中已經(jīng)說過了主卫,下面我們主要來說一下被動(dòng)引用:
引用父類的靜態(tài)字段,只會(huì)引起父類的初始化鹃愤,而不會(huì)引起子類的初始化簇搅。
定義類數(shù)組,不會(huì)引起類的初始化软吐。
引用類的常量瘩将,不會(huì)引起類的初始化。
被動(dò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)字段凹耙,只會(huì)引起父類初始化姿现,而不會(huì)引起子類的初始化 ?
// ?String b = InitClass.b;// 使用類的常量不會(huì)引起類的初始化 ?
SubInitClass[] sc =?new?SubInitClass[10];// 定義類數(shù)組不會(huì)引起類的初始化 ?
}
}
最后總結(jié)一下使用階段:使用階段包括主動(dòng)引用和被動(dòng)引用,主動(dòng)飲用會(huì)引起類的初始化肖抱,而被動(dòng)引用不會(huì)引起類的初始化备典。
當(dāng)使用階段完成之后,java類就進(jìn)入了卸載階段意述。
卸載
在類使用完之后提佣,如果滿足下面的情況,類就會(huì)被卸載:
該類所有的實(shí)例都已經(jīng)被回收荤崇,也就是java堆中不存在該類的任何實(shí)例镐依。
加載該類的ClassLoader已經(jīng)被回收。
該類對(duì)應(yīng)的java.lang.Class對(duì)象沒有任何地方被引用天试,無法在任何地方通過反射訪問該類的方法槐壳。
? ? ? ? 如果以上三個(gè)條件全部滿足,jvm就會(huì)在方法區(qū)垃圾回收的時(shí)候?qū)︻愡M(jìn)行卸載喜每,類的卸載過程其實(shí)就是在方法區(qū)中清空類信息务唐,java類的整個(gè)生命周期就結(jié)束了。
總結(jié)
? ? ? ? 做java的朋友對(duì)于對(duì)象的生命周期可能都比較熟悉带兜,對(duì)象基本上都是在jvm的堆區(qū)中創(chuàng)建枫笛,在創(chuàng)建對(duì)象之前,會(huì)觸發(fā)類加載(加載刚照、連接刑巧、初始化),當(dāng)類初始化完成后,根據(jù)類信息在堆區(qū)中實(shí)例化類對(duì)象啊楚,初始化非靜態(tài)變量吠冤、非靜態(tài)代碼以及默認(rèn)構(gòu)造方法,當(dāng)對(duì)象使用完之后會(huì)在合適的時(shí)候被jvm垃圾收集器回收恭理。讀完本文后我們知道拯辙,對(duì)象的生命周期只是類的生命周期中使用階段的主動(dòng)引用的一種情況(即實(shí)例化類對(duì)象)。而類的整個(gè)生命周期則要比對(duì)象的生命周期長的多颜价。