類的生命周期概述
Java程序的所有數(shù)據(jù)結(jié)構(gòu)和算法都封裝在類型之中扔仓,這也是面向?qū)ο缶幊陶Z言的一大特色褐奥。當(dāng)JVM執(zhí)行一個Java類所封裝的算法之前 ,首先要做的一件事便是字節(jié)碼文件解析翘簇,字節(jié)碼文件解析包含 3 個主要的過程:常量池解析撬码、Java類字段解析及 Java 方法解析。通過類字段解析版保,JVM能夠分析出Java類所封裝的數(shù)據(jù)結(jié)構(gòu)呜笑;通過方法解析,JVM能夠分析出Java類所封裝的算法邏輯 彻犁。而無論是數(shù)據(jù)結(jié)構(gòu)還是方法信息叫胁,很多與“字符串”或者大數(shù)據(jù)(是指占二進(jìn)位比較多的大數(shù))相關(guān)的信息都封裝于常量池中,所以JVM欲解析字段和方法信息袖裕,必先解析常量池曹抬。當(dāng)常量池、字段和方法信息全部被解析完急鳄,則字節(jié)碼文件的“精華”便已經(jīng)被完全消化吸收谤民。但是,這幾個過程其實僅僅屬于Java類 “加載”過程中的一個環(huán)節(jié)疾宏,這對于一個Java類的整個“漫長”的生命周期而言张足,僅僅是個開始。在字節(jié)碼文件的精華被吸收之后還需要經(jīng)過一系列的“二次” 消化處理坎藐,方能被JVM在運(yùn)行期“隨心所欲”地調(diào)用为牍。
按照J(rèn)VM規(guī)范,一個Java文件從被加載到被卸載的整個生命過程岩馍,總共要經(jīng)歷5個階段 : 加載?→?鏈接(驗證+準(zhǔn)備+解析)→?初始化(使用前的準(zhǔn)備)→?使用?→?卸載碉咆。其中第二個階段“鏈接”,對應(yīng)了3個階段 :驗證 蛀恩、準(zhǔn)備和解析疫铜,因此,也有很多典籍說 Java 類的生命周期一共包括7個階段双谆。
前文所講的常量池解析壳咕、Java字段和方法的解析席揽,其實都屬于加載階段的一部分。所謂加 載谓厘,簡而言之就是將 Java 類的字節(jié)碼文件加載到機(jī)器內(nèi)存中并在內(nèi)存中構(gòu)建出Java類的原型類模板對象幌羞。所謂類模板對象,其實就是 Java類在JVM內(nèi)存中的一個快照竟稳,JVM將從字節(jié)碼文件中解析出的常量池属桦、類字段、類方法等信息存儲到類模板中他爸,這樣JVM在運(yùn)行期便能通過類模板而獲取Java類中的任意信息地啰,能夠?qū)?Java 類的成員變量進(jìn)行遍歷,也能進(jìn)行 Java 方法的調(diào)用讲逛。反射的機(jī)制即基于這一基礎(chǔ)。如果 JVM 沒有將 Java 類的聲明信息存儲起來岭埠,則只叫在運(yùn)行期也無法反射盏混。字節(jié)碼相關(guān)的工具類庫,例如 asm 惜论、cglib 等许赃,都利用了這一機(jī)制,在運(yùn)行期動態(tài)修改靜態(tài)聲明的 Java 類所對應(yīng)的字節(jié)碼內(nèi)容馆类,從而在運(yùn)行期直接改掉Java 類的定義混聊,甚至直接在運(yùn)行期創(chuàng)建一個全新的Java類。
Java類是寫給人類看的乾巧,而只叫內(nèi)存中的類模板快照則是寫給機(jī)器“看”的句喜。物理機(jī)器無法直接執(zhí)行Java類的源代碼,所以需要通過類加載這樣一個過程將字節(jié)碼格式的 Java 類轉(zhuǎn)換成機(jī)器能夠識別的內(nèi)存類模板快照沟于。
JVM完成 Java 類加載之后咳胃,接著便開始進(jìn)行鏈接。所謂鏈接旷太,雖然與編譯原理中的鏈接不是同一件事展懈,然而本質(zhì)上是相同的」╄担總體而言存崖,鏈接的主要作用是將字節(jié)碼指令中對常量池中的索引引用轉(zhuǎn)換為直接引用。鏈接包含 3 個步驟 :驗證睡毒、準(zhǔn)備和解析来惧。其實在類加載階段(也即類的生命周期的第一個階段)JVM會對字節(jié)碼文件進(jìn)行驗證,只不過該階段的驗證著重于字節(jié)碼文件格式本身吕嘀,與“鏈接”階段的驗證側(cè)重點不同违寞。在鏈接階段贞瞒,著重于由字節(jié)碼信息出發(fā)進(jìn)行反向驗證,例如趁曼,驗證根據(jù)字節(jié)碼文件中的類名是否能夠找到對應(yīng)的類模板军浆。這些都驗證無誤之后,JVM才能放心地加載當(dāng)前類挡闰,也才能放心地將字節(jié)碼指令中對常量池索引號的引用重寫為直接引用乒融。
在正式使用Java類之前的最后一道工序便是“初始化”,這里所謂的初始化摄悯,并非指對類進(jìn)行實例化赞季,而是指執(zhí)行類的()方法。Java類的實例化奢驯,對應(yīng)的乃是 Java 類 生命周期中的”使用”段申钩。總體而言瘪阁,當(dāng)Java類中包含 static 修飾的靜態(tài)字段 撒遣,或者有使用 static{}塊包裹的代碼段時,編譯后便會在字節(jié)碼文件中包含一個名為()的方法管跺,JVM在初始化階段便會調(diào)用該方法义黎。需要說明一點,該方法僅能由Java編譯器生成并由JVM調(diào)用豁跑,程序開發(fā)者無法自定義一個同名的方法廉涕,更無法直接在Java程序中調(diào)用該方法 雖,該方法也是由字節(jié)碼指令所組成艇拍。
等JVM完成類的初始化之后狐蜕,便“萬事俱備,只欠東風(fēng)”卸夕,就等著開發(fā)者使用了馏鹤。使用方式多種多樣,其中最常見的一種方式是通過new關(guān)鍵字來實例化一個 Java 類娇哆。
當(dāng)然湃累,除了通過new關(guān)鍵字使用java類,還有多種方式碍讨,例如下面的例子:
該示例使用Class.forName(String)接口加載一個類治力,并通過Class.newlnstance()接口實例化一個類。
從廣義上說勃黍,類的加載也可以對應(yīng)類生命周期的7個階段中的前 5個階段即加載宵统、驗證 、 準(zhǔn)備 、解析和初始化马澈。當(dāng)類加載之后瓢省,JVM內(nèi)部會為Java類創(chuàng)建一個對等的類模板,類模板在JDK 6時代被存儲在所謂的perm區(qū)痊班,而到了JDK 8時代勤婚,則被存儲在所謂的metaSpace 區(qū)。無論存儲在哪里涤伐,當(dāng)存儲區(qū)即將被打爆而這個類又不再使用時馒胆,JVM的GC便有可能將其回收萌朱,即釋放內(nèi)存偏陪。而當(dāng)實例化一個 Java 類之后 ,JVM內(nèi)部則會為Java類實例對象創(chuàng)建一個對等的實例對象缺狠,該實例對象所存儲的區(qū)域與具體的 GC 策略緊密關(guān)聯(lián)器净,有可能在新生代型雳,也可能在老年代,當(dāng)然山害,更可能在棧上(棧上分配)四啰。當(dāng)類被使用完畢之后,JVM必須銷毀實例對象粗恢,否則只怕內(nèi)存區(qū)早晚會被打爆。JVM對類模板的銷毀和類實例對象的銷毀欧瘪,都是卸載眷射。
總體而言,Java類的生命周期如圖所示佛掖。
類加載
前文已經(jīng)描述過JVM對字節(jié)碼文件的精華部分的解析過程妖碉,當(dāng)字節(jié)碼文件解析完成之后,JVM便會在內(nèi)部創(chuàng)建一個與Java類對等的類模板對象芥被,說白了該對象其實是 C++類的實例欧宜。每一個Java類模型,最終在只叫內(nèi)部都會有一個 klassOop 與之對等 拴魄,Java 類中的字段 冗茸、方法及 常量池等都會保存到 klassOop 實例對象中。要注意匹中,這個實例對象并非 Java 類的實例對象夏漱,其僅僅用于表示 Java 類型本身,或者 Java 類的定義顶捷。與 Java 類實例對象對等的JVM內(nèi)部對象是instanceOop實例挂绰。
下面就從Java類模板對象instanceKlass 的創(chuàng)建開始講起。
前面描述過Java字節(jié)碼文件的常量池解析 服赎、字段解析與方法解析 葵蒂,這三部分內(nèi)容的解析便是 Java 字節(jié)碼文件的精華所在交播。當(dāng)這 3 個過程執(zhí)行完成之后,Java字節(jié)碼文件的精華便被分析完了践付,至此JVM便對Java類中所定義的一切數(shù)據(jù)結(jié)構(gòu)和算法“了如指掌”秦士,為了鞏固“勝利成果”,JVM需要將這些好不容易辛辛苦苦解析出來的結(jié)果保存起來荔仁。這些解析的結(jié)果會存儲到klassOop這個內(nèi)部類對象實例中伍宦,可以將該對象看作 Java 類在JVM內(nèi)部完全對等的一個鏡像,只不過Java類是寫給人類看的乏梁,而內(nèi)部鏡像 klassOop 則是寫給機(jī)器讀的次洼。當(dāng)成功保存解析結(jié)果之后,則 Java 類的生命周期的第一個階段加載遇骑,便大功告成卖毁。不看過程看結(jié)果,類加載階段其實就是為了這一目標(biāo)而來落萎,在JVM內(nèi)部創(chuàng)建一個與Java類結(jié)構(gòu)對等的數(shù)據(jù)對象亥啦。
從instanceKlass的結(jié)構(gòu)可以看到,其內(nèi)部定義了若干字段练链,這些字段足以存儲 Java 類規(guī)范所支持的一切信息翔脱,例如字段 、方法 媒鼓、內(nèi)部類等届吁,因為 instanceKlass 要作為 Java 類在JVM內(nèi)部對等的結(jié)構(gòu)體,所以能夠兼容Java類中的所有元素是其唯一的設(shè)計目標(biāo)绿鸣。但是 JVM在創(chuàng)建instanceKlass對象時疚沐,為其所申請的內(nèi)存空間卻超過了instanceKlass 類型本身所需的內(nèi)存大小,這是因為JVM需要在instanceKlass內(nèi)存空間的末尾再預(yù)留出足夠的空間潮模,存儲虛方法表 vtable接口表 itable 及 JAVA 類中的引用類型表 oopMap亮蛔。存儲虛方法表 vtable,其作用在前文分析Java
方法的解析機(jī)制時詳細(xì)描述過擎厢,這里不再贅述究流。itable與 oopMap 也是各有其作用。不過靜態(tài)字段在不同的 JDK 版本中存放的位置不同动遭,在JDK 6中梯嗽,靜態(tài)字段會被分配到 instanceKlass 實例對象所申請的內(nèi)存空間中,而在 JDK7和JDK8中沽损,靜態(tài)字段將會被分配到與 instanceKlass對等的鏡像類一 java .lang.Class 實例中灯节,關(guān)于靜態(tài)字段的分配及鏡像類會在下文詳細(xì)分析,此處先略過不表。
圖中的這個內(nèi)存數(shù)據(jù)結(jié)構(gòu)炎疆,便是Java類加載的最終產(chǎn)物卡骂,也是Java類在內(nèi)存中的對等體。JVM根據(jù)這個數(shù)據(jù)結(jié)構(gòu)形入,能夠獲取Java類中所定義的一切元素全跨。
類加載的最終結(jié)果便是在JVM的方法區(qū)創(chuàng)建一個與Java類對等的instanceKlass 實例對象,但是在JVM創(chuàng)建完instanceKlass之后亿遂,又創(chuàng)建了與之對等的另一個鏡像類java.lang.Class浓若。
JVM之所以在instanceKlass之外再創(chuàng)建一個mirror,是有用意的蛇数,總體而言挪钓,java.lang. Class是為了被 Java 程序調(diào)用,而 instanceKlass 則是為了被JVM內(nèi)部訪問耳舅。所以碌上,JVM直接暴露給Java的是 java_mirror,而不是 InstanceKlass 浦徊。
JDK8之所以將靜態(tài)字段從instanceKlass 遷移到mirror中馏予,也不是沒有道理。畢竟靜態(tài)字段并非Java類的成員變量盔性,如果從數(shù)據(jù)結(jié)構(gòu)這個角度看霞丧,靜態(tài)字段不能算作 Java 類這個數(shù)據(jù)結(jié)構(gòu)的一部分,因此 JDK 8 將靜態(tài)字段轉(zhuǎn)移到 mirror 中冕香。從反射的角度看蛹尝,靜態(tài)字段放在 mirror 中是合理的,畢竟在進(jìn)行反射時暂筝,需要給 出 Java 類中所定義的全部字段,無論字段是不是靜態(tài)類型硬贯。