類的生命周期
類加載過程包括:加載-驗(yàn)證-準(zhǔn)備-解析-初始化链快。
這個(gè)過程順序并不是固定的凉敲,最多僅僅代表它們開始的順序着倾,實(shí)際上這五個(gè)過程是交叉進(jìn)行的,通常會(huì)在一個(gè)階段執(zhí)行的過程中調(diào)用令蛉、激活另外一個(gè)階段聚霜。
注意:解析階段有可能在初始化階段之后再開始。
生命周期過程詳解
- 加載:一珠叔、將class文件加載在內(nèi)存中蝎宇;二、將靜態(tài)數(shù)據(jù)結(jié)構(gòu)(數(shù)據(jù)存在于class文件的結(jié)構(gòu))轉(zhuǎn)化成方法區(qū)中運(yùn)行時(shí)的數(shù)據(jù)結(jié)構(gòu)(數(shù)據(jù)存在于JVM時(shí)的數(shù)據(jù)結(jié)構(gòu))祷安;三姥芥、在堆中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象,作為數(shù)據(jù)訪問的入口汇鞭。
- 驗(yàn)證:確保加載的類符合JVM規(guī)范與安全凉唐。
- 準(zhǔn)備:為static變量在方法區(qū)中分配空間,設(shè)置變量的初始值霍骄。例如static int a=3台囱,在此階段會(huì)a被初始化為0,其他數(shù)據(jù)類型參考成員變量聲明读整。
- 解析:虛擬機(jī)將常量池的符號(hào)引用轉(zhuǎn)變成直接引用簿训。例如"aaa"為常量池的一個(gè)值,直接把"aaa"替換成存在于內(nèi)存中的地址米间。
- 符號(hào)引用:符號(hào)引用以一組符號(hào)來描述所引用的目標(biāo)强品,符號(hào)可以是任何形 式的字面量,只要使用時(shí)能無歧義地定位到目標(biāo)即可屈糊。符號(hào)引用與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局無關(guān)的榛,引用 的目標(biāo)并不一定已經(jīng)加載到內(nèi)存中。
- 直接引用:直接引用可以是直接指向目標(biāo)的指針逻锐、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄夫晌。直接引用是與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局相關(guān)的雕薪,如果有了直接引用,那么引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在晓淀。
- 初始化:初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法蹦哼。在類構(gòu)造器方法中,它將由編譯器自動(dòng)收集類中的所有類變量的賦值動(dòng)作(準(zhǔn)備階段的a正是被賦值a)和靜態(tài)變量與靜態(tài)語句塊static{}合并要糊,初始化時(shí)機(jī)后續(xù)再聊。
- 使用:正常使用妆丘。
- 卸載:GC把無用對(duì)象從內(nèi)存中卸載锄俄。
類初始化時(shí)機(jī)
只有當(dāng)對(duì)類主動(dòng)使用的時(shí)候才會(huì)導(dǎo)致類的初始化。
主動(dòng)引用(發(fā)生類初始化過程)
- 創(chuàng)建類的實(shí)例勺拣,也就是new的方式
- 訪問某個(gè)類或接口的靜態(tài)變量奶赠,或者對(duì)該靜態(tài)變量賦值
- 調(diào)用類的靜態(tài)方法
- 反射(如Class.forName(“com.shengsiyuan.Test”))
- 初始化某個(gè)類的子類,則其父類也會(huì)被初始化
- Java虛擬機(jī)啟動(dòng)時(shí)被標(biāo)明為啟動(dòng)類的類(Java Test)药有,直接使用java.exe命令來運(yùn)行某個(gè)主類
被動(dòng)引用(不會(huì)發(fā)生類的初始化)
- 通過子類引用父類的靜態(tài)字段毅戈,只會(huì)觸發(fā)父類的初始化,而不會(huì)觸發(fā)子類的初始化愤惰。
- 定義對(duì)象數(shù)組苇经,不會(huì)觸發(fā)該類的初始化。
- 常量在編譯期間會(huì)存入調(diào)用類的常量池中宦言,本質(zhì)上并沒有直接引用定義常量的類扇单,不會(huì)觸發(fā)定義常量所在的類。
- 通過類名ClassName.class獲取Class對(duì)象奠旺,不會(huì)觸發(fā)類的初始化蜘澜。
- Class.forName("類名全路徑")加載指定類時(shí),如果指定參數(shù)initialize為false時(shí)响疚,也不會(huì)觸發(fā)類初始化鄙信,其實(shí)這個(gè)參數(shù)是告訴虛擬機(jī),是否要對(duì)類進(jìn)行初始化忿晕。
- 通過ClassLoader默認(rèn)的loadClass方法装诡,也不會(huì)觸發(fā)初始化動(dòng)作。
類加載器
主要的類加載器有
- 啟動(dòng)類加載器(Bootstrap ClassLoader):由C++實(shí)現(xiàn)杏糙,沒有父類慎王,負(fù)責(zé)加載 JAVA_HOME\lib 目錄中的,或通過-Xbootclasspath參數(shù)指定路徑中的宏侍,且被虛擬機(jī)認(rèn)可(按文件名識(shí)別赖淤,如rt.jar)的類。
- 擴(kuò)展類加載器(Extension ClassLoader):由Java語言實(shí)現(xiàn)谅河,父類加載器為null咱旱,負(fù)責(zé)加載 JAVA_HOME\lib\ext 目錄中的确丢,或通過java.ext.dirs系統(tǒng)變量指定路徑中的類庫。
- 應(yīng)用程序類加載器(Application ClassLoader):由Java語言實(shí)現(xiàn)吐限,父類加載器為ExtClassLoader鲜侥,負(fù)責(zé)加載用戶路徑(classpath)上的類庫。
- 自定義加載器:可以通過繼承java.lang.ClassLoader或URLClassLoader實(shí)現(xiàn)自定義的類加載器诸典,父類加載器為AppClassLoader描函。
雙親委派模式
JVM使用的是雙親委派模式的類加載機(jī)制,這種機(jī)制的好處有:
- 避免類的重復(fù)加載
- 保證核心基礎(chǔ)jar包加載的安全問題狐粱。
加載過程大致可以分為兩個(gè)過程:
- 查找過程舀寓,首先從收到請(qǐng)求的加載器開始,從下到上查找是否加載過該類肌蜻,從下到上的順序:自定義類加載器->應(yīng)用類加載器->拓展類加載器->啟動(dòng)類加載器互墓;
- 加載過程,如果找不到加載過該類蒋搜,則從上到下篡撵,從啟動(dòng)類加載器開始嘗試從自己負(fù)責(zé)的路徑下加載該類,如果加載失敗則由下級(jí)加載器加載豆挽,從上到下的順序:啟動(dòng)類加載器 -> 拓展類加載器 ->應(yīng)用類加載器 -> 自定義類加載器育谬。
加載器類圖
從類圖可以看出
- 拓展類加載器ExtClassLoader和系統(tǒng)類加載器AppClassLoader,這兩個(gè)類都繼承自URLClassLoader祷杈,是sun.misc.Launcher的靜態(tài)內(nèi)部類斑司。
- sun.misc.Launcher主要被系統(tǒng)用于啟動(dòng)主應(yīng)用程序,ExtClassLoader和AppClassLoader都是由sun.misc.Launcher創(chuàng)建的但汞。
- ExtClassLoader并沒有重寫loadClass()方法宿刮,而AppClassLoader重載了loadCass()方法,但最終調(diào)用的還是父類loadClass()方法私蕾,因此依然遵守雙親委派模式僵缺。
自定義加載器
方式
- 繼承URLClassLoader,不需要重寫 findClass()等方法踩叭,繼承雙親委派模式磕潮。
- 繼承ClassLoader,只重寫findClass方法容贝,這種方式繼承雙親委派模式自脯。
- 繼承ClassLoader,重寫loadClass方法斤富,不使用雙親委派模式膏潮,自定義類加載模式。
自定義加載器場景:
- 當(dāng)class文件不在ClassPath路徑下满力,默認(rèn)系統(tǒng)類加載器無法找到該class文件焕参,在這種情況下我們需要實(shí)現(xiàn)一個(gè)自定義的ClassLoader來加載特定路徑下的class文件生成class對(duì)象轻纪。
- 當(dāng)一個(gè)class文件是通過網(wǎng)絡(luò)傳輸并且可能會(huì)進(jìn)行相應(yīng)的加密操作時(shí),需要先對(duì)class文件進(jìn)行相應(yīng)的解密后再加載到JVM內(nèi)存中叠纷,這種情況下也需要編寫自定義的ClassLoader并實(shí)現(xiàn)相應(yīng)的邏輯刻帚。
- 當(dāng)需要實(shí)現(xiàn)熱部署功能時(shí)(一個(gè)class文件通過不同的類加載器產(chǎn)生不同class對(duì)象從而實(shí)現(xiàn)熱部署功能),需要實(shí)現(xiàn)自定義ClassLoader的邏輯涩嚣。
注意:由于雙親委托機(jī)制崇众,classpath目錄下的類默認(rèn)由Application類加載器加載,所以航厚,自定義加載器如果沒有重寫loadClass方法去加載classpath下的類是不會(huì)成功的校摩,當(dāng)然,可以通過直接調(diào)findClass()繞過雙親委托來加載阶淘。
雙親委派模型的破壞者1——線程上下文加載器
在Java應(yīng)用中存在著很多服務(wù)提供者接口(Service Provider Interface,SPI)互妓,這些接口允許第三方為它們提供實(shí)現(xiàn)溪窒,如常見的 SPI 有 JDBC、JNDI等冯勉,這些 SPI 的接口屬于 Java 核心庫澈蚌,一般存在rt.jar包中,由Bootstrap類加載器加載灼狰,而 SPI 的第三方實(shí)現(xiàn)代碼則是作為Java應(yīng)用所依賴的 jar 包被存放在classpath路徑下宛瞄,由于SPI接口中的代碼經(jīng)常需要加載具體的第三方實(shí)現(xiàn)類并調(diào)用其相關(guān)方法,但SPI的核心接口類是由引導(dǎo)類加載器來加載的交胚,而Bootstrap類加載器無法直接加載SPI的實(shí)現(xiàn)類份汗,同時(shí)由于雙親委派模式的存在,Bootstrap類加載器也無法反向委托AppClassLoader加載器SPI的實(shí)現(xiàn)類蝴簇。在這種情況下杯活,我們就需要一種特殊的類加載器來加載第三方的類庫,而線程上下文類加載器就是很好的選擇熬词。
加載方式
- 顯式加載:指的是在代碼中通過調(diào)用ClassLoader加載class對(duì)象旁钧,比如代碼中通過Class.forName()、this.getClass.getClassLoader.LoadClass()互拾,自定義類加載器中的findClass()方法等歪今。
- 隱式加載:不直接在代碼中調(diào)用ClassLoader的方法加載class對(duì)象,而是通過虛擬機(jī)自動(dòng)加載到內(nèi)存中颜矿,除了顯式加載的寄猩,其它都是隱式加載。
ClassLoader關(guān)鍵方法
- loadClass(String)或衡,該方法加載指定名稱(包括包名)的二進(jìn)制類型焦影,該方法在JDK1.2之后不再建議用戶重寫但用戶可以直接調(diào)用該方法车遂,loadClass()方法是ClassLoader類自己實(shí)現(xiàn)的,該方法中的邏輯就是雙親委派模式的實(shí)現(xiàn)斯辰,所以舶担,自定義加載器可以覆蓋loadClass方法覆蓋雙親委派模式。
- findClass(String) 彬呻,在自定義類加載器時(shí)衣陶,會(huì)直接覆蓋ClassLoader的findClass()方法并編寫加載規(guī)則,取得要加載類的字節(jié)碼后轉(zhuǎn)換成流闸氮,然后調(diào)用defineClass()方法生成類的Class對(duì)象剪况。
- defineClass(byte[] b, int off, int len) ,是用來將byte字節(jié)流解析成JVM能夠識(shí)別的Class對(duì)象(ClassLoader中已實(shí)現(xiàn)該方法邏輯)蒲跨,通過這個(gè)方法不僅能夠通過class文件實(shí)例化class對(duì)象译断,也可以通過其他方式實(shí)例化class對(duì)象,如通過網(wǎng)絡(luò)接收一個(gè)類的字節(jié)碼或悲,然后轉(zhuǎn)換為byte字節(jié)流創(chuàng)建對(duì)應(yīng)的Class對(duì)象孙咪。
- resolveClass(Class??? c) ,類解析階段的操作方法巡语,給Classloader鏈接一個(gè)類翎蹈。
注意:
如果直接調(diào)用defineClass()或findClass()方法生成類的Class對(duì)象,這個(gè)類的Class對(duì)象并沒有解析(也可以理解為鏈接階段男公,畢竟解析是鏈接的最后一步)荤堪,其解析操作需要等待初始化階段進(jìn)行。
重復(fù)類加載
Java中來自不同Jar包中的相同的類名(包名枢赔,類名)在加載時(shí)類加載器將按照Class Path中的順序加載澄阳,相同的類名僅僅會(huì)加載一次,順序排在前面的類會(huì)被加載踏拜,加載成功后寇荧,后面再遇到相同類會(huì)忽略。
因此执隧,最終所使用的類取決于ClassLoader對(duì)類的的選擇揩抡,即Maven往Class Path打包的順序。
類卸載
JVM中的Class只有滿足以下三個(gè)條件镀琉,才能被GC回收峦嗤,也就是該Class被卸載(unload):
- 該類所有的實(shí)例都已經(jīng)被GC。
- 該類的java.lang.Class對(duì)象沒有在任何地方被引用屋摔。
- 加載該類的ClassLoader實(shí)例已經(jīng)被GC烁设。
所以
- 由Java虛擬機(jī)自帶的類加載器(啟動(dòng)類加載器、擴(kuò)展類加載器、應(yīng)用程序類加載器)所加載的類装黑,在虛擬機(jī)的生命周期中副瀑,始終不會(huì)被卸載。因?yàn)镴ava虛擬機(jī)本身會(huì)始終引用這些類加載器恋谭,而這些類加載器則會(huì)始終引用它們所加載的類的Class對(duì)象糠睡,因此這些Class對(duì)象始終是可觸及的。
- 由用戶自定義的類加載器加載的類是可以被卸載的疚颊,如下圖使用自定義加載器加載的類和對(duì)象狈孔,只要左側(cè)三個(gè)引用變量置為null,類加載器生命周期結(jié)束材义、加載的Class對(duì)象生命周期結(jié)束均抽、Sample對(duì)象生命周期結(jié)束,方法區(qū)的類信息也會(huì)被卸載其掂。
是否同個(gè)類
在JVM中表示兩個(gè)class對(duì)象是否為同一個(gè)類對(duì)象存在兩個(gè)必要條件
- 類的完整類名必須一致油挥,包括包名。
- 加載這個(gè)類的ClassLoader(指ClassLoader實(shí)例對(duì)象)必須相同款熬。
獲取Class對(duì)象的三種方式
Class類是反射機(jī)制的起源喘漏,我們得到Class類對(duì)象有3種方法:
第一種:通過類名獲得
Class<?> class = ClassName.class;
第二種:通過類名全路徑獲得:
Class<?> class = Class.forName("類名全路徑");
第三種:通過實(shí)例對(duì)象獲得:
Class<?> class = object.getClass();
Class.forName()和ClassLoader.loadClass()區(qū)別
- Class.forName():將類的.class文件加載到j(luò)vm中之外,還會(huì)對(duì)類進(jìn)行解釋华烟,執(zhí)行類中的static塊;
- ClassLoader.loadClass():只干一件事情持灰,就是將.class文件加載到j(luò)vm中盔夜,不會(huì)執(zhí)行static中的內(nèi)容,只有在newInstance才會(huì)去執(zhí)行static塊。
注:
Class.forName(name, initialize, loader)帶參函數(shù)也可控制是否加載static塊堤魁。并且只有調(diào)用了newInstance()方法采用調(diào)用構(gòu)造函數(shù)喂链,創(chuàng)建類的對(duì)象 。