虛擬機(jī)把Class文件加載到內(nèi)存,并對數(shù)據(jù)進(jìn)行校驗(yàn)孟岛、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型督勺,這就是虛擬機(jī)的類加載機(jī)制渠羞。
在Java語言里,類型的加載和連接過程都是在程序運(yùn)行期間完成的智哀,這樣會(huì)在類加載時(shí)稍微增加一些性能開銷次询,但是卻能提供更好的靈活性。
類加載的時(shí)機(jī)
類從加載到虛擬機(jī)內(nèi)存開始到卸載出內(nèi)存為止瓷叫,其生命周期主要包括:Loading屯吊、Verification、Preparation摹菠、Resolution雌芽、Initialization、Using辨嗽、Unloading等階段。其中Verification淮腾、Preparation糟需、Resolution等三個(gè)階段合為Linking階段。
什么情況下需要開始Loading階段虛擬機(jī)規(guī)范中并沒有進(jìn)行強(qiáng)制約束谷朝,但是對于初始化階段洲押,迅即規(guī)范則嚴(yán)格規(guī)定了有且只有四種情況必須立即對類進(jìn)行初始化(Initialization):
- 遇到new、getstatic圆凰、putstatic或invokestatic這4調(diào)字節(jié)碼指令的時(shí)候杈帐,如果類沒有初始化,則需要先出發(fā)其初始化专钉。其對應(yīng)的java代碼一般是使用new創(chuàng)建對象挑童,讀取或?qū)o態(tài)變量賦值(常量池靜態(tài)變量除外)以及調(diào)用一個(gè)類的靜態(tài)方法的時(shí)候。
- 使用java.lang.reflect包的方法對類進(jìn)行反射調(diào)用的時(shí)候
- 當(dāng)初始化一個(gè)類的時(shí)候需先初始化其父類
- 虛擬機(jī)啟動(dòng)的時(shí)候會(huì)初始化執(zhí)行主類(含main方法跃须,且是程序入口)
對于這四種場景中外的情況站叼,都屬于被動(dòng)引用,不會(huì)觸發(fā)初始化菇民。
對于靜態(tài)字段尽楔,其被訪問的時(shí)候投储,只有直接定義這個(gè)字段的類才會(huì)被初始化,因此通過其子類來引用父類中定義的靜態(tài)字段阔馋,只會(huì)觸發(fā)父類的初始化玛荞,而不會(huì)觸發(fā)子類的初始化。
通過數(shù)組定義來引用類呕寝,不會(huì)觸發(fā)此類的初始化egSuperClass[] sca = new SuperClass[10];
并不會(huì)初始化SuperClass
對字符串常量的引用會(huì)轉(zhuǎn)化成對自己類文件中常量池的中常量的引用勋眯,因此也不會(huì)初始化被引用的類。
接口中不能使用static{}語句塊壁涎,但編譯器仍然會(huì)為接口生成"<clinit>()'類構(gòu)造器凡恍,用于初始化接口中定義的成員變量。接口初始化時(shí)并不要求其父接口全部初始化怔球,只有在真正使用到父接口的時(shí)候才進(jìn)行初始化嚼酝。
類加載的過程
加載(Loading)
加載階段虛擬機(jī)需要完成以下三件事情:
- 通過一個(gè)類的權(quán)限定名來獲取定義此類的二進(jìn)制字節(jié)流
- 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法去的運(yùn)行時(shí)的數(shù)據(jù)結(jié)構(gòu)
- 在Java堆中生成一個(gè)代表這個(gè)類的Class對象,作為方法去這些數(shù)據(jù)的訪問入口
加載類的二進(jìn)制流竟坛,其來源是 不限制的闽巩,可以來自文件、網(wǎng)絡(luò)担汤、數(shù)據(jù)庫涎跨、運(yùn)行時(shí)生成等。加載完成時(shí)崭歧,虛擬機(jī)外部的二進(jìn)制字節(jié)流就按照虛擬機(jī)所需的格式存儲(chǔ)在方法區(qū)之中隅很,方法區(qū)中的數(shù)據(jù)存儲(chǔ)格式由虛擬機(jī)實(shí)現(xiàn)自行定義,虛擬機(jī)規(guī)范未規(guī)定此區(qū)域的具體數(shù)據(jù)結(jié)構(gòu)率碾。然后在Java堆中實(shí)例化一個(gè)Class類的對象叔营,這個(gè)對象將作為俄日程序訪問方法區(qū)中的這些類型數(shù)據(jù)的外部接口。加載階段與連接階段的部分內(nèi)容(如一部分字節(jié)碼文件格式驗(yàn)證動(dòng)作)是交叉進(jìn)行的所宰。
校驗(yàn)(Verification)
校驗(yàn)是連接階段的第一步绒尊,其目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全仔粥。虛擬機(jī)對輸入的字節(jié)流不符合Class文件的存儲(chǔ)格式就拋出一個(gè)java.lang.VerifyError異秤て祝或者其子類。
文件格式的驗(yàn)證
這一階段主要驗(yàn)證字節(jié)流是否符合Class文件格式規(guī)范躯泰,并且能被當(dāng)前版本的虛擬機(jī)處理谭羔。可能包括:
- 是否以魔數(shù)0xCAFEBABE開頭斟冕。
- 主次版本號(hào)是否在當(dāng)前虛擬機(jī)處理的范圍內(nèi)
- 常量池的常量中是否有不被支持的常量類型
- 指向常量的各種索引值中是否有指向不存在的常量或者不符合類型的常量
- CONSTANT_Urf8_info型常量中是否有不符合UTF8編碼的數(shù)據(jù)
- Class文件中各個(gè)部分及文件本身是否有被刪除或附加的其他信息
- ...
經(jīng)過這個(gè)階段的驗(yàn)證后口糕,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法區(qū)中進(jìn)行存儲(chǔ),所以后面三個(gè)驗(yàn)證階段全部是基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)進(jìn)行的磕蛇。
元數(shù)據(jù)驗(yàn)證
第二階段是對字節(jié)碼描述的信息進(jìn)行語義分析景描,以保證信息符合Java語言規(guī)范的要求
- 這個(gè)類是否有父類(除Object之外十办,所有的類都應(yīng)當(dāng)有父類)
- 這個(gè)類是否繼承了不允許被繼承的類
- 如果這個(gè)類不是抽象類,是否實(shí)現(xiàn)了其父類或接口中要求實(shí)現(xiàn)的全部方法
- 類中的字段超棺、方法是否與父類產(chǎn)生了矛盾
- ...
字節(jié)碼驗(yàn)證
第三階段是最復(fù)雜的一個(gè)階段向族,主要工作是進(jìn)行數(shù)據(jù)流和控制流分析。這個(gè)類主要對類中方法體進(jìn)行校驗(yàn)分析棠绘。這個(gè)階段的任務(wù)是保證被校驗(yàn)的方法運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的行為:
- 保證任意時(shí)刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作件相,例如不會(huì)出現(xiàn)操作棧上放置一個(gè)int類型的數(shù)據(jù),使用時(shí)卻按long類型來加載入本地變量表中氧苍。
- 保證跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體外的字節(jié)碼指令上
- 保證方法體中的類型轉(zhuǎn)換是有效的夜矗。例如把子類賦給父類是合法的反之則非法
- ...
由于數(shù)據(jù)流驗(yàn)證的高復(fù)雜性,虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)為了避免將過多的時(shí)間消耗在字節(jié)碼驗(yàn)證階段让虐。在JDK1.6之后Javac編譯器中進(jìn)行了一項(xiàng)優(yōu)化紊撕,給方法體的Code屬性的屬性表中新增了一個(gè)“StackMapTable”的屬性,這項(xiàng)屬性描述了方法體重所有的基本塊開始時(shí)本地變量表和操作棧應(yīng)有的狀態(tài)赡突,這可以將字節(jié)碼驗(yàn)證的類型推導(dǎo)轉(zhuǎn)變?yōu)轭愋蜋z查從而節(jié)省一些時(shí)間对扶。
符號(hào)引用驗(yàn)證
對后一個(gè)階段的校驗(yàn)發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候,這個(gè)轉(zhuǎn)化動(dòng)作在解析階段中發(fā)生惭缰。符號(hào)引用驗(yàn)證可以看作是對類自身以外的信息進(jìn)行匹配性的校驗(yàn):
- 符號(hào)引用中通過字符串描述的全限定名是否能找到對應(yīng)的類
- 在指定類中是否存在符合方法的字段描述符及簡單名稱所描述的方法和字段浪南。
- 符號(hào)引用中的類、字段和方法的訪問控制權(quán)限是否允許被當(dāng)前類訪問漱受。
符號(hào)引用驗(yàn)證的目的是保證解析動(dòng)作能正常運(yùn)行络凿。
校驗(yàn)階段對于虛擬機(jī)類加載機(jī)制來說是一個(gè)非常重要的但非必要的階段,可以通過-Xverify:none參數(shù)來關(guān)閉大部分類驗(yàn)證措施昂羡,以縮短虛擬機(jī)類加載的時(shí)間喷众。
準(zhǔn)備(Preparation)階段
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類初始變量初始值的階段,這些內(nèi)存都將在方法區(qū)中進(jìn)行分配紧憾。這個(gè)階段內(nèi)存分配只包含類變量而不包括實(shí)例變量,實(shí)例變量將會(huì)在對象實(shí)例化時(shí)隨著對象一起分配在Java堆中昌渤。其次是變量初始值為默認(rèn)值赴穗。例如public static int value = 123
準(zhǔn)備階段后初始值為0而不是123因?yàn)檫@個(gè)時(shí)候尚未開始執(zhí)行任何Java代碼,而value賦值的指令時(shí)編譯后放到<clinit>()方法中的膀息。如果字段的字段屬性表中存在ConstantValue屬性般眉,那再準(zhǔn)備階段變量value就會(huì)被初始化為ConstantValue屬性指定的值,如public static final int value = 123
在準(zhǔn)備階段就會(huì)被直接賦值
解析(Resolution)
解析階段是虛擬機(jī)將常量池中的符號(hào)引用替換為直接引用的過程潜支。
符號(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)的指針,相對偏移量或是一個(gè)能簡介定位到目標(biāo)的句柄项玛。直接引用是與虛擬機(jī)實(shí)現(xiàn)的內(nèi)存布局相關(guān)的貌笨,同一個(gè)符號(hào)引用在不同虛擬機(jī)實(shí)力上翻譯出來的直接引用一般不會(huì)相同。如果有了直接引用襟沮,那引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在了锥惋。
關(guān)于符號(hào)引用和直接引用的區(qū)別可參考https://www.zhihu.com/question/30300585
虛擬機(jī)規(guī)范中并未規(guī)定解析階段發(fā)生的具體時(shí)間,只要求在執(zhí)行anewarray开伏、checkcast膀跌、個(gè)體field
getstatic、instanceof固灵、invokeinterface捅伤、invokespecial、invokestatic怎虫、invokevirtual暑认、multianewarray、new大审、putfield和putstatic13個(gè)用于操作符號(hào)引用的字節(jié)碼指令之前蘸际,先對他們所適應(yīng)的符號(hào)引用進(jìn)行解析。所以虛擬機(jī)實(shí)現(xiàn)會(huì)根據(jù)需要來判斷徒扶,到底是在類被加載時(shí)就對常量池中的符號(hào)引用進(jìn)行解析還是等到一個(gè)符號(hào)引用將要使用前才去解析它粮彤。解析動(dòng)作做主要針對類或接口、字段姜骡、類方法导坟、接口方法 四類符號(hào)引用進(jìn)行,分別對應(yīng)于常量池的CONSTANT_Class_info圈澈、CONSTANT_Fieldref_info惫周、CONSTANT_Methodref_info及CONSTANT_InterfaceMethodref_info四種常量類型。
類或者接口的解析
假設(shè)當(dāng)前代碼所處的類為D康栈,如果要把一個(gè)從未解析過的復(fù)活藥引用N解析為一個(gè)類或者接口C的直接引用递递,那虛擬機(jī)完成整個(gè)解析的過程包括以下3個(gè)步驟:
- 如果C不是一個(gè)數(shù)組類型,那虛擬機(jī)將會(huì)把代表N的權(quán)限定名傳遞給D的類加載器去加載C這個(gè)類。在加載過程中啥么,由于無數(shù)據(jù)驗(yàn)證登舞、字節(jié)碼驗(yàn)證的需要,又將可能觸發(fā)其他相關(guān)類的加載動(dòng)作悬荣,例如加載這個(gè)類的父類或?qū)崿F(xiàn)的接口菠秒。一旦這個(gè)加載過程出現(xiàn)了任何異常,解析過程就將宣告失敗氯迂。
- 如果C是一個(gè)數(shù)組類型践叠,并且數(shù)組的元素類型為對象言缤,那將會(huì)按照第一條的規(guī)則加載數(shù)組元素類型。接著由虛擬機(jī)生成一個(gè)代表此數(shù)組維度和元素的數(shù)組對象酵熙。
- 如果上面的步驟沒有出現(xiàn)任何異常轧简,那么C在虛擬機(jī)中實(shí)際上已經(jīng)成為一個(gè)有效的類或者接口了,但在解析完成前還需要進(jìn)行符號(hào)引用驗(yàn)證匾二,確認(rèn)D是否具備對C的訪問權(quán)限哮独,如果發(fā)現(xiàn)不具備訪問權(quán)限將拋出 java.lang.IllegalAccessError.
字段解析
首先根據(jù)class_index解析其類型,如果解析完成察藐。先查找類本身皮璧,而后自上而下查找接口最后自上而下查找父類。如果查找失敗拋出NoSuchFieldError.如果查找成功分飞,將會(huì)對這個(gè)字段進(jìn)行權(quán)限驗(yàn)證悴务。
類方法解析
先根據(jù)class_index索引解析所屬的類或者接口的符號(hào)引用,如果解析成功譬猫。按照以下順序?qū)Ψ椒ㄟM(jìn)行搜索:如果在類方法表中發(fā)現(xiàn)class_index中索引是個(gè)接口讯檐,直接拋出java.lang.IncompatibleClassChangeError.而后在類中查找,繼續(xù)在父類中遞歸查找染服,如果沒找到則到此類實(shí)現(xiàn)的接口或者父接口中遞歸查找别洪,如果找到說明此類為抽象類。如果沒查找到則拋出NoSuchMethodError柳刮。
接口方法解析
與類解析相反挖垛,如果在接口方法中發(fā)現(xiàn)class_index中的索引是個(gè)類而不是接口直接拋出IncompatibleClassChangeError。而后在接口本身內(nèi)部查找秉颗,進(jìn)而在接口的父接口中遞歸查找痢毒,如果都查不到則拋出NoSuchMethodError異常。
初始化(Initialization)
初始化階段才真正開始執(zhí)行類中定義的Java程序代碼
初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程蚕甥。
- <clinit>()方法是由編譯器自動(dòng)收集類中的所有類變量的賦值動(dòng)作和靜態(tài)語句塊中的語句合并產(chǎn)生的哪替,編譯器手機(jī)的順序是由語句在源文件中出現(xiàn)的順序所決定的,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量菇怀,定義在它之后的變量夷家,在前面的靜態(tài)語句塊中可以賦值,但是不能訪問敏释。
- <clinit>()方法與類的構(gòu)造函數(shù)不同,它不需要顯式地調(diào)用父類的構(gòu)造器摸袁,虛擬機(jī)會(huì)保證在子類的<clinit>()方法執(zhí)行之前钥顽,父類的<clinit>()方法已經(jīng)執(zhí)行完畢。
- 如果一個(gè)類中沒有靜態(tài)語句塊靠汁,也沒有對變量的賦值操作蜂大,那么編譯器可以不為這個(gè)類生成<clinit>()方法
- 接口中不能使用靜態(tài)代碼塊闽铐,但仍然有變量初始化的賦值操作,接口的<clinit>()方法調(diào)用的時(shí)候不一定需要調(diào)用父接口對應(yīng)的方法奶浦。而接口的實(shí)現(xiàn)類在初始化時(shí)也一樣不會(huì)調(diào)用接口的<clinit>()方法兄墅。
- 虛擬機(jī)會(huì)保證一個(gè)類的<clinit>()方法在多線程環(huán)境中被正確地加鎖和同步,如果多個(gè)線程同時(shí)去初始化一個(gè)類澳叉,那么只會(huì)有一個(gè)線程回去執(zhí)行這個(gè)類的<clint>()方法隙咸,其他線程都需要阻塞等待。
類加載器
類加載器最初為了Java Applet來設(shè)計(jì)成洗,目前在類層次劃分五督,OSGi,熱部署瓶殃,代碼加密等領(lǐng)域應(yīng)用廣泛充包。
類與類加載器
對于任一類,都需要由加載它的類加載器和這個(gè)類本身一同確立其在java虛擬機(jī)中的唯一性遥椿。如果不注意Class的唯一性標(biāo)識(shí)基矮,可能會(huì)導(dǎo)致Class對象的equals, isAssignableFrom方法,isInstance等方法返回結(jié)果與預(yù)期不同冠场。
雙親委派模型
站在Java虛擬機(jī)的角度講家浇,只存在兩種不同的類加載器:一種是啟動(dòng)類的加載器(Bootstrap ClassLoader)這個(gè)類加載器使用C++實(shí)現(xiàn),是虛擬機(jī)自身的一部分慈鸠。另一種是所有其他類的加載器蓝谨,這些來加載器都由Java語言實(shí)現(xiàn),獨(dú)立于虛擬機(jī)外部青团,并且全部繼承自java.lang.ClassLoader
絕大部分Java程序會(huì)使用到以下三種類加載器
- Bootstrap ClassLoader:這個(gè)類加載器負(fù)責(zé)將存放在<JAVA_HOME>\lib目錄中或者被-Xbootclasspath參數(shù)指定的路徑中的譬巫,并且是虛擬機(jī)識(shí)別的(僅按照文件名識(shí)別)類庫加載到虛擬機(jī)中,此加載器無法被Java程序直接引用督笆。
- Extension ClassLoader:這個(gè)類加載器由sum.misc.Launcher$ExtClassLoader實(shí)現(xiàn)芦昔,它負(fù)責(zé)加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫娃肿。
- Application ClassLoader:這個(gè)加載器由sun.misc.Launcher$AppClassLoader來實(shí)現(xiàn)咕缎。由于這個(gè)類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也成為系統(tǒng)類加載器料扰。它負(fù)責(zé)加載用戶類路徑上指定的類庫凭豪,開發(fā)者可以使用這個(gè)類加載器。一般情況下這個(gè)基于是程序中默認(rèn)的類加載器晒杈。
類加載器之間的這種層次關(guān)系嫂伞,成為類加載器的雙親委派模型。雙親委派模型要求除了Bootstrap ClassLoader其余的類加載器都應(yīng)當(dāng)有自己的父類加載器。這些類加載器之間一般不使用繼承而是使用組合來復(fù)用類加載器代碼帖努。一般的做法是只有父類加載器無法完成加載動(dòng)作的時(shí)候撰豺,子類加載器才會(huì)真正的去加載某個(gè)類。這樣保證了Java繼承體系的正常運(yùn)作拼余。