java編譯與類加載機(jī)制

不論是物理機(jī)還是虛擬機(jī),大部分的程序代碼從開始編譯到最終轉(zhuǎn)化成物理機(jī)的目標(biāo)代碼或虛擬機(jī)能執(zhí)行的指令集之前,都會(huì)按照如下圖所示的各個(gè)步驟進(jìn)行:



其中綠色的模塊可以選擇性實(shí)現(xiàn)?;谖锢頇C(jī)、虛擬機(jī)等的語言,大多都遵循這種基于現(xiàn)代經(jīng)典編譯原理的思路,在執(zhí)行前先對程序源碼進(jìn)行詞法解析和語法解析處理,把源碼轉(zhuǎn)化為抽象語法樹。

1.javac編譯

javac 編譯器將.java文件編譯成為.class文件。這里的 javac 編譯器稱為前端編譯器,其他的前端編譯器還有諸如 Eclipse JDT 中的增量式編譯器 ECJ 等。相對應(yīng)的還有后端編譯器,它在程序運(yùn)行期間將字節(jié)碼轉(zhuǎn)變成機(jī)器碼,如 HotSpot 虛擬機(jī)自帶的 JIT編譯器(分 Client 端和 Server 端)。另外,還有可能會(huì)碰到靜態(tài)提前編譯器(AOT,Ahead Of Time Compiler)直接把*.java文件編譯成本地機(jī)器代碼。

詞法、語法分析

詞法分析是將源代碼的字符流轉(zhuǎn)變?yōu)闃?biāo)記集合。單個(gè)字符是程序編寫過程中的的最小元素,而標(biāo)記則是編譯過程的最小元素,關(guān)鍵字、變量名、字面量、運(yùn)算符等都可以成為標(biāo)記,比如整型標(biāo)志 int 由三個(gè)字符構(gòu)成,但是它只是一個(gè)標(biāo)記,不可拆分。

語法分析是根據(jù)Token序列來構(gòu)造抽象語法樹的過程。抽象語法樹是一種用來描述程序代碼語法結(jié)構(gòu)的樹形表示方式,語法樹的每一個(gè)節(jié)點(diǎn)都代表著程序代碼中的一個(gè)語法結(jié)構(gòu),如 bao、類型、修飾符、運(yùn)算符等。經(jīng)過這個(gè)步驟后,編譯器就基本不會(huì)再對源碼文件進(jìn)行操作了,后續(xù)的操作都建立在抽象語法樹之上。

填充符號表

完成了語法分析和詞法分析之后,下一步就是填充符號表的過程。符號表是由一組符號地址和符號信息構(gòu)成的表格。符號表中所登記的信息在編譯的不同階段都要用到,在語義分析中,符號表所登記的內(nèi)容將用于語義檢查和產(chǎn)生中間代碼,在目標(biāo)代碼生成階段,黨對符號名進(jìn)行地址分配時(shí),符號表是地址分配的依據(jù)。

語義分析

語法樹能表示一個(gè)結(jié)構(gòu)正確的源程序的抽象,但無法保證源程序是符合邏輯的。而語義分析的主要任務(wù)是讀結(jié)構(gòu)上正確的源程序進(jìn)行上下文有關(guān)性質(zhì)的審查。語義分析過程分為標(biāo)注檢查和數(shù)據(jù)及控制流分析。

  • 標(biāo)注檢查步驟檢查的內(nèi)容包括諸如變量使用前是否已被聲明、變量和賦值之間的數(shù)據(jù)類型是否匹配等。
  • 數(shù)據(jù)及控制流分析是對程序上下文邏輯更進(jìn)一步的驗(yàn)證,它可以檢查出諸如程序局部變量在使用前是否有賦值、方法的每條路徑是否都有返回值、是否所有的受查異常都被正確處理了等問題。
字節(jié)碼生成

字節(jié)碼生成是 javac 編譯過程的最后一個(gè)階段。字節(jié)碼生成階段不僅僅是把前面各個(gè)步驟所生成的信息轉(zhuǎn)化成字節(jié)碼寫到磁盤中,編譯器還進(jìn)行了少量的代碼添加和轉(zhuǎn)換工作。 實(shí)例構(gòu)造器方法和類構(gòu)造器<clinit>()方法就是在這個(gè)階段添加到語法樹之中的(這里的實(shí)例構(gòu)造器并不是指默認(rèn)的構(gòu)造方法,而是指我們自己重載的構(gòu)造方法,如果代碼中沒有提供任何構(gòu)造方法,那編譯器會(huì)自動(dòng)添加一個(gè)沒有參數(shù)、訪問權(quán)限與當(dāng)前類一致的默認(rèn)構(gòu)造方法,這個(gè)工作在填充符號表階段就已經(jīng)完成了)。

2.JIT編譯

Java 程序最初是僅僅通過解釋器解釋執(zhí)行的,即對字節(jié)碼逐條解釋執(zhí)行,這種方式的執(zhí)行速度相對會(huì)比較慢,尤其當(dāng)某個(gè)方法或代碼塊運(yùn)行的特別頻繁時(shí),這種方式的執(zhí)行效率就顯得很低。于是后來在虛擬機(jī)中引入了 JIT 編譯器(即時(shí)編譯器),當(dāng)虛擬機(jī)發(fā)現(xiàn)某個(gè)方法或代碼塊運(yùn)行特別頻繁時(shí),就會(huì)把這些代碼認(rèn)定為“Hot Spot Code”(熱點(diǎn)代碼),為了提高熱點(diǎn)代碼的執(zhí)行效率,在運(yùn)行時(shí),虛擬機(jī)將會(huì)把這些代碼編譯成與本地平臺相關(guān)的機(jī)器碼,并進(jìn)行各層次的優(yōu)化。
HotSpot 虛擬機(jī)中內(nèi)置了兩個(gè)JIT編譯器:Client Complier 和 Server Complier,分別用在客戶端和服務(wù)端,目前主流的 HotSpot 虛擬機(jī)中默認(rèn)是采用解釋器與其中一個(gè)編譯器直接配合的方式工作。
運(yùn)行過程中會(huì)被即時(shí)編譯器編譯的“熱點(diǎn)代碼”有兩類:

  • 被多次調(diào)用的方法。
  • 被多次調(diào)用的循環(huán)體。

目前主要的熱點(diǎn) 判定方式有以下兩種:

  • 基于采樣的熱點(diǎn)探測:采用這種方法的虛擬機(jī)會(huì)周期性地檢查各個(gè)線程的棧頂,如果發(fā)現(xiàn)某些方法經(jīng)常出現(xiàn)在棧頂,那這段方法代碼就是“熱點(diǎn)代碼”。這種探測方法的好處是實(shí)現(xiàn)簡單高效,還可以很容易地獲取方法調(diào)用關(guān)系,缺點(diǎn)是很難精確地確認(rèn)一個(gè)方法的熱度,容易因?yàn)槭艿骄€程阻塞或別的外界因素的影響而擾亂熱點(diǎn)探測。
  • 基于計(jì)數(shù)器的熱點(diǎn)探測:采用這種方法的虛擬機(jī)會(huì)為每個(gè)方法,甚至是代碼塊建立計(jì)數(shù)器,統(tǒng)計(jì)方法的執(zhí)行次數(shù),如果執(zhí)行次數(shù)超過一定的閥值,就認(rèn)為它是“熱點(diǎn)方法”。這種統(tǒng)計(jì)方法實(shí)現(xiàn)復(fù)雜一些,需要為每個(gè)方法建立并維護(hù)計(jì)數(shù)器,而且不能直接獲取到方法的調(diào)用關(guān)系,但是它的統(tǒng)計(jì)結(jié)果相對更加精確嚴(yán)謹(jǐn)。
    HotSpot 虛擬機(jī)中使用的是基于計(jì)數(shù)器的熱點(diǎn)探測方法,它為每個(gè)方法準(zhǔn)備了兩個(gè)計(jì)數(shù)器:方法調(diào)用計(jì)數(shù)器和回邊計(jì)數(shù)器。

方法調(diào)用計(jì)數(shù)器用來統(tǒng)計(jì)方法調(diào)用的次數(shù),在默認(rèn)設(shè)置下,方法調(diào)用計(jì)數(shù)器統(tǒng)計(jì)的并不是方法被調(diào)用的絕對次數(shù),而是一個(gè)相對的執(zhí)行頻率,即一段時(shí)間內(nèi)方法被調(diào)用的次數(shù)。
回邊計(jì)數(shù)器用于統(tǒng)計(jì)一個(gè)方法中循環(huán)體代碼執(zhí)行的次數(shù)(準(zhǔn)確地說,應(yīng)該是回邊的次數(shù),因?yàn)椴⒎撬械难h(huán)都是回邊),在字節(jié)碼中遇到控制流向后跳轉(zhuǎn)的指令就稱為“回邊”。

3.類加載機(jī)制

類是在運(yùn)行期間第一次使用時(shí)動(dòng)態(tài)加載的,而不是一次性加載所有類。類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個(gè)生命周期包括:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用和卸載七個(gè)階段。


類的生命周期

類的加載過程主要包含了加載、驗(yàn)證、準(zhǔn)備、解析和初始化這 5 個(gè)階段。如下:

加載

加載過程完成以下三件事:

  • 通過一個(gè)類的全限定名來獲取其定義的二進(jìn)制字節(jié)流。
  • 將這個(gè)字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
  • 在堆中生成一個(gè)代表這個(gè)類的 Class 對象,作為對方法區(qū)中這些數(shù)據(jù)的訪問入口。
驗(yàn)證

驗(yàn)證的目的是為了確保 Class 文件中的字節(jié)流包含的信息符合當(dāng)前虛擬機(jī)的要求,而且不會(huì)危害虛擬機(jī)自身的安全。

  • 文件格式的驗(yàn)證:驗(yàn)證字節(jié)流是否符合 Class 文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理,該驗(yàn)證的主要目的是保證輸入的字節(jié)流能正確地解析并存儲于方法區(qū)之內(nèi)。經(jīng)過該階段的驗(yàn)證后,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法區(qū)中進(jìn)行存儲,后面的三個(gè)驗(yàn)證都是基于方法區(qū)的存儲結(jié)構(gòu)進(jìn)行的。
  • 元數(shù)據(jù)驗(yàn)證:對類的元數(shù)據(jù)信息進(jìn)行語義校驗(yàn)(其實(shí)就是對類中的各數(shù)據(jù)類型進(jìn)行語法校驗(yàn)),保證不存在不符合 Java 語法規(guī)范的元數(shù)據(jù)信息。
  • 字節(jié)碼驗(yàn)證:該階段驗(yàn)證的主要工作是進(jìn)行數(shù)據(jù)流和控制流分析,對類的方法體進(jìn)行校驗(yàn)分析,以保證被校驗(yàn)的類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的行為。
  • 符號引用驗(yàn)證:它發(fā)生在虛擬機(jī)將符號引用轉(zhuǎn)化為直接引用的時(shí)候(解析階段中發(fā)生該轉(zhuǎn)化,后面會(huì)有講解),主要是對類自身以外的信息(常量池中的各種符號引用)進(jìn)行匹配性的校驗(yàn)。
準(zhǔn)備

準(zhǔn)備階段為類變量分配內(nèi)存并設(shè)置初始值,使用的是方法區(qū)的內(nèi)存。


基本類型及引用類型初始值
解析

解析階段是虛擬機(jī)將常量池中的符號引用轉(zhuǎn)化為直接引用的過程。
這里說一下符合引用于直接引用的區(qū)別:

  • 符號引用:符號引用以一組符號來描述所引用的目標(biāo),符號可以是任何形式的字面量,只要使用時(shí)能無歧義地定位到目標(biāo)即可。符號引用與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局無關(guān),引用的目標(biāo)并不一定已經(jīng)加載到了內(nèi)存中。
  • 直接引用:直接引用可以是直接指向目標(biāo)的指針、相對偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。直接引用是與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局相關(guān)的,同一個(gè)符號引用在不同虛擬機(jī)實(shí)例上翻譯出來的直接引用一般不會(huì)相同。如果有了直接引用,那說明引用的目標(biāo)必定已經(jīng)存在于內(nèi)存之中了。
    解析動(dòng)作主要針對類或接口、字段、類方法、接口方法四類符號引用進(jìn)行。
  • 類或接口的解析:判斷所要轉(zhuǎn)化成的直接引用是對數(shù)組類型,還是普通的對象類型的引用,從而進(jìn)行不同的解析。
  • 字段解析:對字段進(jìn)行解析時(shí),會(huì)先在本類中查找是否包含有簡單名稱和字段描述符都與目標(biāo)相匹配的字段,如果有,則查找結(jié)束;如果沒有,則會(huì)按照繼承關(guān)系從上往下遞歸搜索該類所實(shí)現(xiàn)的各個(gè)接口和它們的父接口,還沒有,則按照繼承關(guān)系從上往下遞歸搜索其父類,直至查找結(jié)束。
  • 類方法解析:對類方法的解析與對字段解析的搜索步驟差不多,只是多了判斷該方法所處的是類還是接口的步驟,而且對類方法的匹配搜索,是先搜索父類,再搜索接口。
  • 接口方法解析:與類方法解析步驟類似,知識接口不會(huì)有父類,因此,只遞歸向上搜索父接口就行了。
class Super{  
    public static int m = 11;  
    static{  
        System.out.println("執(zhí)行了super類靜態(tài)語句塊");  
    }  
}  

class Father extends Super{  
    public static int m = 33;  
    static{  
        System.out.println("執(zhí)行了父類靜態(tài)語句塊");  
    }  
}  

class Child extends Father{  
    static{  
        System.out.println("執(zhí)行了子類靜態(tài)語句塊");  
    }  
}  

public class StaticTest{  
    public static void main(String[] args){  
        System.out.println(Child.m);  
    }  
}  

執(zhí)行結(jié)果如下:

執(zhí)行了super類靜態(tài)語句塊
執(zhí)行了父類靜態(tài)語句塊
33
初始化

初始化是類加載過程的最后一步,才真正開始執(zhí)行類中定義的 Java 程序代碼。在準(zhǔn)備階段,類變量已經(jīng)賦過一次系統(tǒng)要求的初始值,而在初始化階段,根據(jù)程序員通過程序制定的主觀計(jì)劃去初始化類變量和其它資源。初始化階段是執(zhí)行類構(gòu)造器<clinit>() 方法的過程。
<clinit>()執(zhí)行規(guī)則:

  • <clinit>()是由編譯器自動(dòng)收集類中的所有類變量的賦值動(dòng)作和靜態(tài)語句塊中的語句合并產(chǎn)生的,編譯器收集的順序是由語句在源文件中出現(xiàn)的順序所決定的,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語句中可以賦值,但是不能訪問。
  • <clinit>()方法與類的構(gòu)造方法不同,它不需要顯式地調(diào)用父類構(gòu)造器,虛擬機(jī)會(huì)保證在子類的()方法執(zhí)行之前,父類的()方法已經(jīng)執(zhí)行完畢。因此,在虛擬機(jī)中第一個(gè)被執(zhí)行的()方法的類肯定是java.lang.Object。
  • <clinit>()方法對于類或接口來說并不是必須的,如果一個(gè)類中沒有靜態(tài)語句塊,也沒有對類變量的賦值操作,那么編譯器可以不為這個(gè)類生成<clinit>()方法。
  • 接口中不能使用靜態(tài)語句塊,但仍然有類變量初始化的賦值操作,因此接口與類一樣會(huì)生成<clinit>()方法。但是接口與類不同的是:執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父接口的<clinit>()方法,只有當(dāng)父接口中定義的變量被使用時(shí),父接口才會(huì)被初始化。另外,接口的實(shí)現(xiàn)類在初始化時(shí)也一樣不會(huì)執(zhí)行接口的<clinit>()方法。
  • 虛擬機(jī)會(huì)保證一個(gè)類的<clinit>()方法在多線程環(huán)境中被正確地加鎖和同步,如果多個(gè)線程同時(shí)去初始化一個(gè)類,那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類的<clinit>()方法,其他線程都需要阻塞等待,直到活動(dòng)線程執(zhí)行<clinit>()方法完畢。如果在一個(gè)類的<clinit>()方法中有耗時(shí)很長的操作,那就可能造成多個(gè)線程阻塞,在實(shí)際應(yīng)用中這種阻塞往往是很隱蔽的。

4.類加載時(shí)機(jī)

  • 創(chuàng)建類的實(shí)例,也就是new一個(gè)對象
  • 訪問某個(gè)類或接口的靜態(tài)變量,或者對該靜態(tài)變量賦值
  • 調(diào)用類的靜態(tài)方法
  • 反射(Class.forName(""))
  • 初始化一個(gè)類的子類(會(huì)首先初始化子類的父類)
  • JVM啟動(dòng)時(shí)標(biāo)明的啟動(dòng)類

5.類加載器

類加載器細(xì)致的可以劃分為:

  • 啟動(dòng)類加載器(Bootstrap ClassLoader)此類加載器負(fù)責(zé)將存放在 <JRE_HOME>\lib 目錄中的,或者被 -Xbootclasspath 參數(shù)所指定的路徑中的,并且是虛擬機(jī)識別的(僅按照文件名識別,如 rt.jar,名字不符合的類庫即使放在 lib 目錄中也不會(huì)被加載)類庫加載到虛擬機(jī)內(nèi)存中。啟動(dòng)類加載器無法被 Java 程序直接引用,用戶在編寫自定義類加載器時(shí),如果需要把加載請求委派給啟動(dòng)類加載器,直接使用 null 代替即可。
  • 擴(kuò)展類加載器(Extension ClassLoader)這個(gè)類加載器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實(shí)現(xiàn)的。它負(fù)責(zé)將 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系統(tǒng)變量所指定路徑中的所有類庫加載到內(nèi)存中,開發(fā)者可以直接使用擴(kuò)展類加載器。
  • 應(yīng)用程序類加載器(Application ClassLoader)這個(gè)類加載器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)實(shí)現(xiàn)的。由于這個(gè)類加載器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般稱為系統(tǒng)類加載器。它負(fù)責(zé)加載用戶類路徑(ClassPath)上所指定的類庫,開發(fā)者可以直接使用這個(gè)類加載器,如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個(gè)就是程序中默認(rèn)的類加載器。
  • 自定義類加載器(Custom ClassLoader)如果有必要,我們還可以加入自定義的類加載器。因?yàn)?JVM 自帶的 ClassLoader 只是懂得從本地文件系統(tǒng)加載標(biāo)準(zhǔn)的 java class 文件。
雙親委派模型

雙親委派模型的工作流程是:如果一個(gè)類加載器收到了類加載的請求,它首先不會(huì)自己去嘗試加載這個(gè)類,而是把請求委托給父加載器去完成,依次向上,因此,所有的類加載請求最終都應(yīng)該被傳遞到頂層的啟動(dòng)類加載器中,只有當(dāng)父加載器在它的搜索范圍中沒有找到所需的類時(shí),即無法完成該加載,子加載器才會(huì)嘗試自己去加載該類。
這里的父子關(guān)系一般通過組合關(guān)系(Composition)來實(shí)現(xiàn),而不是繼承關(guān)系(Inheritance)



使用雙親委派模型來組織類加載器之間的關(guān)系,有一個(gè)很明顯的好處,就是 Java 類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系,這對于保證 Java 程序的穩(wěn)定運(yùn)作很重要。例如,類java.lang.Object 類存放在 rt.jar 之中,因此無論是哪個(gè)類加載器要加載此類,最終都會(huì)委派給啟動(dòng)類加載器進(jìn)行加載,這就保證了 Object 類在程序中的各種類加載器中都是同一個(gè)類。

參考資料
周志明. 深入理解 Java 虛擬機(jī) [M]. 機(jī)械工業(yè)出版社, 2011.
《深入理解Java虛擬機(jī):JVM高級特性與最佳實(shí)踐》第二版
深入探討 Java 類加載器

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容