本文轉載自 面試官:請你談談Java的類加載過程
一個Java文件從編碼完成到最終執(zhí)行,一般包括兩個過程
- 編譯
- 運行
編譯:把我們寫好的java文件,通過javac命令編譯成字節(jié)碼,也就是我們常說的.class文件
運行:則把編譯生成.class文件交由Java虛擬機(JVM)執(zhí)行。
而我們所說的類加載過程就是指JVM把.class文件中類信息加載到內(nèi)存中,并進行解析生成對應的class對象的過程。
舉個通俗的例子來說,JVM在執(zhí)行某段代碼時,遇到了class A,然后此時內(nèi)存中并沒有class A 的相關信息,于是JVM就會到相應的class文件中去尋找class A的類信息,并加載到內(nèi)存中,這就是我們所說的類加載過程。
由此可見,JVM不是一開始就把所有的類加載到內(nèi)存中,而是只有第一次遇到某個需要運行的類才會加載。
類加載
- 加載
- 連接
- 初始化
而連接又可以細分為三個小部分 - 驗證
- 準備
- 解析
加載
簡單來說,加載指的是把class字節(jié)碼文件從各個來源通過類加載器裝載入內(nèi)存中。
這里有兩個重點:
- 字節(jié)碼來源。一般的加載來源包括從本地路徑下編譯生成的.class文件,從jar包中.class文件,從遠程網(wǎng)絡,以及動態(tài)代理實時編譯
- 類加載器。一般包括啟動類加載器,擴展類加載器,應用類加載器,以及用戶的自定義類加載器。
注:為什么會自定義類加載器?
- 一方面是由于Java代碼很容易被反編譯,如果需要對自己的代碼加密的話,可以對編譯后的代碼進行加密,然后再通過實現(xiàn)自己的自定義加載器進行解密,最后再加載。
- 另一方面也有可能可能從非標準的來源加載代碼,比如從網(wǎng)絡來源,那就需要自己實現(xiàn)一個類加載器,從指定源進行加載。
驗證
主要是為了保證加載進來的字節(jié)流符合虛擬機規(guī)范,不會造成安全錯誤。
包括對于文件格式的驗證,比如常量中是否有不被支持的常量,文件中是否有不規(guī)范的或者附加的其他信息?
對于元數(shù)據(jù)的驗證,比如該類是否繼承了被final修飾的類?類中的字段,方法是否與父類沖突?是否出現(xiàn)了不合理的重載?
對于字節(jié)碼的驗證,保證程序語義的合理性,比如要保證類型轉換的合理性。
對于符號引用的驗證,比如校驗符號引用中通過全限定名是否能夠找到對應的類,校驗符號引用中的訪問性(private,public等)是否被當前類訪問。
準備
主要是為類變量(注意,不是實例變量)分配內(nèi)存,并且賦予初值。
特別需要注意,初值,不是代碼中具體些的初始化的值,而是java虛擬機根據(jù)不同變量類型的默認初始值
比如8種引用類型,默認為0;引用類型的初值為null;常量的初值為代碼中設置的值。
解析
將常量池內(nèi)的符號引用替換為直接引用的過程。
兩個重點:
- 符號引用 即一個字符串,但是這個字符串給出了一些能夠唯一性識別一個方法,一個類的相關信息
- 直接引用 可以理解為一個內(nèi)存地址,或者一個偏移量。比如類方法,類變量的直接引用是指向方法區(qū)的指針,而實例方法,實例變量的直接引用則是從實例的頭指針開始算起到這個實例變量位置的偏移量。
舉個例子:現(xiàn)在調用方法hello(),這個方法的地址是1234567,那么hello就是符號引用,1234567就是直接引用。
在解析階段,虛擬機會把所有的類名,方法名,字段名這么符號引用替換為具體的內(nèi)存地址或偏移量,也就是直接引用。
初始化
這個階段主要是對類變量的初始化,是執(zhí)行類構造器的過程。
換句話說,只對static修飾的變量或語句進行初始化。
如果初始化一個類的時候,其父類尚未初始化,則優(yōu)先初始化其父類。
如果同時包含多個靜態(tài)變量和靜態(tài)代碼塊,則按照自上而下的順序依次執(zhí)行。
總結
類加載過程只是一個類聲明周期的一部分,在其前,有編譯的過程,只有對源代碼編譯之后,才能獲得能夠被虛擬機加載的字節(jié)碼文件;在其后還有具體的類使用過程,當使用完成之后,還會在方法區(qū)垃圾回收的過程中進行卸載。
類加載器
類加載器是用來加載Java類到java虛擬機中,一般來說,java虛擬機使用java類的方式如下:java源程序(.java文件)在經(jīng)歷java編譯器之后就轉為java字節(jié)代碼(.class文件)。類加載器負責讀取java字節(jié)碼,并轉換成java.lang.Class類的一個實例。每個這樣的實例用來表示一個java類,通過此實例的newInstance()方法就可以創(chuàng)建該類的一個對象。
類加載器應用在很多方面,比如類層次劃分,OSGI,熱部署,代碼加密等領域。
基本上所有的類加載器都是Java.lang.classLoader類的一個實例。
java.lang.ClassLoader類
java.lang.ClassLoader類的基本職責就是根據(jù)一個指定的類的名稱,找到或者生成其對應的字節(jié)碼,然后從這些字節(jié)碼中定義出一個java類,即java.lang.class類的一個實例。除此之外,ClassLoader還負責加載Java應用所需的資源,如圖像文件和配置文件等。
類與類加載器
類加載器雖然只用于實現(xiàn)類的加載動作,但它在java程序中起到的作用卻遠遠不限于類加載階段。對于任意一個類,都需要由加載它的類加載器和這個類本身一起確立其在java虛擬機中的唯一性。
類加載的種類
- 啟動類加載器: 負責加載JRE的核心類庫,如jre目標下的rt.jar,charsets.jar等
- 擴展類加載器:負責加載JRE擴展目錄ext中的jar類包
- 系統(tǒng)類加載器:負責加載classpath路徑下的類包
- 用戶自定義加載器:負責加載用戶自定義路徑下的類包
雙親委派模型
如果一個類加載器收到了類加載請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父類加載器反饋給自己無法完成這個加載請求時(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。