摘要:虛擬機(jī)為了保證一個(gè)類的方法在多線程環(huán)境中被正確地加鎖同步。但啟動(dòng)類加載器不可能認(rèn)識(shí)這些代碼。實(shí)現(xiàn)模塊化熱部署的關(guān)鍵則是它的自定義類加載器機(jī)制的實(shí)現(xiàn)。
概念區(qū)分:
加載、類加載、類加載器
類加載是一個(gè)過程。
加載(Loading)是類加載這一個(gè)過程的階段。
類加載器是ClassLoader類或其子類。
本文中的”類“的描述都包括了類和接口的可能性,因?yàn)槊總€(gè)Class文件都有可能代表Java語言中的一個(gè)類或接口。
本文中的”Class文件“并非特指存在于具體磁盤中的文件,更準(zhǔn)確理解應(yīng)該是一串二進(jìn)制的字節(jié)流。
類加載過程分為:
加載 Loading(注意,別與類加載混淆,類加載是個(gè)過程,加載是其一個(gè)階段)
驗(yàn)證 Verification
準(zhǔn)備 Preparation
解析 Resolution
初始化 Initialization
加載在這個(gè)階段,主要完成3件事:
通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流。 不一定要從本地的Class文件獲取,可以從jar包,網(wǎng)絡(luò),甚至十六進(jìn)制編輯器弄出來的。開發(fā)人員可以重寫類加載器的loadClass()方法。
將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
在內(nèi)存中生產(chǎn)一個(gè)代表這個(gè)類的java.lang.Class對象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口
驗(yàn)證這一階段目的為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。
文件格式驗(yàn)證,如魔數(shù)(0xCAFEBABE)開頭、主次版本號是否在當(dāng)前虛擬機(jī)處理范圍之內(nèi)等。
元數(shù)據(jù)驗(yàn)證,此階段開始就不是直接操作字節(jié)流,而是讀取方法區(qū)里的信息,元數(shù)據(jù)驗(yàn)證大概就是驗(yàn)證是否符合Java語言規(guī)范
字節(jié)碼驗(yàn)證,是整個(gè)驗(yàn)證過程中最復(fù)雜的一個(gè)階段,主要目的是通過數(shù)據(jù)流和控制流分析,確定程序語義是否合法,符合邏輯。JDK6之后做了優(yōu)化,不在驗(yàn)證,可以通過-XX:-UseSplitVerifier關(guān)閉優(yōu)化。
符號引用驗(yàn)證,此階段可以看做是類自己身意外的信息進(jìn)行匹配性校驗(yàn)。
準(zhǔn)備此階段正是為 類變量 分配內(nèi)存和設(shè)置 類變量 初始值的階段。這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。注意這里僅包括 類變量(被static修飾的變量),而不是包括實(shí)例變量。
public static int value = 123;
在這個(gè)階段中,value的值是0
以下是基本數(shù)據(jù)類型的零值
數(shù)據(jù)類型 | 零值 | 數(shù)據(jù)類型 | 零值 |
---|---|---|---|
int | 0 | boolean | false |
long | 0L | float | 0.0f |
short | (short)0 | double | 0.0d |
char | "u0000" | reference | null |
byte | (byte)0 |
特殊情況
public static final int value = 123;
編譯時(shí)javac將會(huì)為value生產(chǎn)ConstantValue屬性,在準(zhǔn)備階段虛擬機(jī)會(huì)根據(jù)ConstatnValue的設(shè)置,將value賦值為123;。
解析這個(gè)階段有點(diǎn)復(fù)雜,我還講不清,先跳過。 //TODO 2017年10月29日
初始化類初始化是類加載過程的最后一步。在前面的類加載過程中,除了在加載階段,用戶應(yīng)用程序可以通過自己定義類加載參與之外,其余動(dòng)作完全由虛擬機(jī)主導(dǎo)和控制。
到了這個(gè)初始化階段,才真正開始執(zhí)行類中定義的Java程序代碼(或者說是字節(jié)碼)。
在編譯時(shí),編譯器或自動(dòng)收集 類 中的所有類變量(被static修飾的變量)的賦值操作和靜態(tài)語句塊中的語句合并,從而生成出一個(gè)叫
staic class Parent{ public static int A = 1; static{ A = 2; } } static class Sub extends Parent{ public static int B = A; } public static void main(String[] args){ System.out.println(Sub.B); //result: 2 }
虛擬機(jī)為了保證一個(gè)類的
重頭戲來了,了解上面的類加載過程之后,我們對類加載有個(gè)感性的認(rèn)識(shí),于是我們可以使用類加載器去決定如何去獲取所需的類。
雖然類加載器僅僅實(shí)現(xiàn)類的加載動(dòng)作(階段),但它在Java程序中起到的作用遠(yuǎn)遠(yuǎn)不限于類加載階段。
對于任意一個(gè)類,都需要由加載它的類加載器和這個(gè)類本身一同確立其在Java虛擬機(jī)中的唯一性。
也就是說,判斷兩個(gè)類是否”相等“(這個(gè)“相等”包括類的Class對象的equals()方法、isAssignableForm()方法、isInstance()方法的返回結(jié)果,也包括使用instanceof關(guān)鍵字做對象所屬關(guān)系的判定),只有在兩個(gè)類是由同一個(gè)類加載器加載的前提下才有意義,否則,即使這兩個(gè)類來源于同一個(gè)Class文件,被同一個(gè)虛擬機(jī)加載,只要它們的類加載器不一樣,那么這兩個(gè)類就必定不同。
package com.jc.jvm.classloader; import java.io.IOException; import java.io.InputStream; /** * 類加載器與instanceof關(guān)鍵字例子 * */ public class ClassLoaderTest { public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException { //定義類加載器 ClassLoader myLoader = new ClassLoader() { @Override public Class> loadClass(String name) throws ClassNotFoundException { String fileName = name.substring(name.lastIndexOf(".")+1)+".class"; // 只需要ClassLoaderTest.class InputStream in = getClass().getResourceAsStream(fileName); if(in==null){ return super.loadClass(name); } byte[] b = new byte[0]; try { b = new byte[in.available()]; in.read(b); } catch (IOException e) { throw new ClassNotFoundException(name); } return defineClass(name,b,0,b.length); } }; //使用類加載器 Object obj = myLoader.loadClass("com.jc.jvm.classloader.ClassLoaderTest").newInstance(); //判斷class是否相同 System.out.println(obj.getClass()); System.out.println(obj instanceof com.jc.jvm.classloader.ClassLoaderTest); } } /**output: * com.jc.jvm.classloader.ClassLoaderTest * false * */雙親委派模型
大概了解類加載器是什么東西之后。我們來了解下,從JVM角度來看,有哪些類加載器。
從JVM的角度來講,只存在兩種不同的類加載器:
啟動(dòng)類加載器(Bootstrap ClassLoader),這個(gè)類加載器是使用C++語言實(shí)現(xiàn),是虛擬機(jī)自身的一部分。
另一種就是其他的類加載器,這些類加載器都由Java語言實(shí)現(xiàn),獨(dú)立于虛擬機(jī)外部,并且都繼承自抽象類java.lang.ClassLoader
而從Java開發(fā)人員的角度來看,類加載器還可以劃分得跟細(xì)致些:
啟動(dòng)類加載器(Bootstrap ClassLoader): 這個(gè)類加載器負(fù)責(zé)將存放在$JAVA_HOME/lib目錄下的,并且是虛擬機(jī)識(shí)別的(僅按照文件名識(shí)別,如rt.jar,名字不符合的類庫即使放在lib目錄下也不會(huì)被加載)類庫加載到虛擬機(jī)內(nèi)存中??梢员?Xbootclasspath參數(shù)修改。啟動(dòng)類加載器無法被Java程序直接引用。
擴(kuò)展類加載器(Extension ClassLoader):這個(gè)加載器由sun.misc.Lancher$ExtClassLoader實(shí)現(xiàn),負(fù)責(zé)加載$JAVA_HOME/lib/ext目錄下的,或者被java.ext.dirs系統(tǒng)變量指定的路徑中的所有類庫。開發(fā)者可以直接使用擴(kuò)展類加載器。
應(yīng)用程序加載器(Application ClassLoader):這個(gè)類加載器由sum.misc.Launcher$AppClassLoader實(shí)現(xiàn)。由于這個(gè)類加載器是ClassLoader的getSystemClassLoader()方法的返回值,所以一般也稱它為 系統(tǒng)類加載器。如果應(yīng)用程序中沒有自定義過自己的類加載器,則使用該類加載器作為默認(rèn)。它負(fù)責(zé)加載用戶類路徑(ClassPath)上所指定的類庫。
再加上自定義類加載器,那么它們之間的層次關(guān)系為:雙親委托模型(Parents Delegation Model)。雙親委托模型要求除了頂層的啟動(dòng)類加載器外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器。這里類加載器之間的父子關(guān)系一般不會(huì)以集成繼承(Inheritance)的關(guān)系來實(shí)現(xiàn),而是都使用組合(Composition)關(guān)系來服用父加載器的代碼。
類加載的雙親委派模型實(shí)在JDK1.2期間引入的,但它不是一個(gè)強(qiáng)制性的約束模型,而是Java設(shè)計(jì)者推薦給開發(fā)者的一種類加載器實(shí)現(xiàn)方式。
雙親委派模型的工作過程是:如果一個(gè)類加載器收到類加載的請求,它首先不會(huì)自己去嘗試加載這個(gè)類,而是把這個(gè)請求委派給父類加載器去完成,每一個(gè)層次的類加載器都是如此。因此所有的加載請求最終都應(yīng)該傳送到頂層的啟動(dòng)類加載器中,只有當(dāng)父加載器反饋?zhàn)约簾o法完成這個(gè)加載請求(它的搜索范圍中沒有找到所需的類)時(shí),子加載器才會(huì)嘗試自己去加載。
雙親委派模型的實(shí)現(xiàn):
protected Class> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class. c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } }雙親委派模型的破壞
由于雙親委派模型不是一個(gè)強(qiáng)制性的約束模型,而是Java設(shè)計(jì)者推薦給開發(fā)者的類加載器實(shí)現(xiàn)方式。因?yàn)闅v史原因和需求不同于是出現(xiàn)過3次破壞:
由于java.lang.ClassLoader在JDK1.0就已經(jīng)存在,而用戶去繼承ClassLoader,就是為覆寫loadClass()方法,而這個(gè)方法實(shí)現(xiàn)有雙親委派模型的邏輯。于是這樣被覆蓋,雙親委派模型就被打破了。于是Java設(shè)計(jì)者在JDK1.2給ClassLoader添加一個(gè)新的方法findClass(),提倡大家應(yīng)當(dāng)把自己的類加載邏輯寫到findClass()方法中,這樣就不會(huì)破壞雙親委派模型的規(guī)則。因?yàn)閘oadClass()方法的邏輯里就是如果父類加載失敗,則會(huì)調(diào)用自己的findClass()來完成加載,請看上面雙親委派模型的實(shí)現(xiàn)。
雙親委派很好地解決了各個(gè)類加載器的基礎(chǔ)類的統(tǒng)一問題,但如果是基礎(chǔ)類,但啟動(dòng)類加載器不認(rèn)得怎么辦。 如JNDI服務(wù),JNDI在JDK1.3開始就作為平臺(tái)服務(wù),它的代碼是由啟動(dòng)類加載器加載(JDK1.3時(shí)放進(jìn)去的rt.jar),但JNDI的目的就是對資源進(jìn)行集中管理和查找,它需要調(diào)用由獨(dú)立廠商實(shí)現(xiàn)并不熟在應(yīng)用的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)代碼。但啟動(dòng)類加載器不可能”認(rèn)識(shí)“這些代碼。
于是Java設(shè)計(jì)團(tuán)隊(duì)引入一個(gè)不太優(yōu)雅的設(shè)計(jì):就是線程上下文類加載器(Thread Context ClassLoader),這個(gè)類加載器可以設(shè)置,但默認(rèn)是就是應(yīng)用程序類加載器。有了這個(gè) 線程上下文類加載器(這名字有點(diǎn)長) 后,就可以做一些”舞弊“的事情(我喜歡稱為hack),JNDI服務(wù)使用這個(gè)線程上下類加載器去加載所需要的SPI代碼,也就是父類加載器請求子類加載其去完成類加載的動(dòng)作。 于是又一次違背了雙親委派模型。詳情請參考:javax.naming.InitialContext的源碼。這里大概放出代碼:
//javax.naming.spi.NamingManager public static Context getInitialContext(Hashtable,?> env) throws NamingException { InitialContextFactory factory; InitialContextFactoryBuilder builder = getInitialContextFactoryBuilder(); if (builder == null) { // No factory installed, use property // Get initial context factory class name String className = env != null ? (String)env.get(Context.INITIAL_CONTEXT_FACTORY) : null; if (className == null) { NoInitialContextException ne = new NoInitialContextException( "Need to specify class name in environment or system " + "property, or as an applet parameter, or in an " + "application resource file: " + Context.INITIAL_CONTEXT_FACTORY); throw ne; } try { factory = (InitialContextFactory) helper.loadClass(className).newInstance(); //這個(gè)helper就是類加載器 } catch(Exception e) { NoInitialContextException ne = new NoInitialContextException( "Cannot instantiate class: " + className); ne.setRootCause(e); throw ne; } } else { factory = builder.createInitialContextFactory(env); } return factory.getInitialContext(env); }
//獲取線程上下文類加載器 ClassLoader getContextClassLoader() { return AccessController.doPrivileged( new PrivilegedAction() { public ClassLoader run() { ClassLoader loader = Thread.currentThread().getContextClassLoader(); //線程類加載器 if (loader == null) { // Don"t use bootstrap class loader directly! loader = ClassLoader.getSystemClassLoader(); } return loader; } } ); }
這次破壞就嚴(yán)重咯,是由于用戶對程序動(dòng)態(tài)性的追求而導(dǎo)致的。也就是:代碼替換(HotSwap)、模塊熱部署(Hot Deployment)等。
對于模塊化之爭有,Sun公司的Jigsaw項(xiàng)目和OSGi組織的規(guī)范。
目前來看OSGi語句成為了業(yè)界的Java模塊化標(biāo)準(zhǔn)。
OSGi實(shí)現(xiàn)模塊化熱部署的關(guān)鍵則是它的自定義類加載器機(jī)制的實(shí)現(xiàn)。每一個(gè)程序模塊(OSGi中成為Bundle)都有一個(gè)自己的類加載器。 當(dāng)需要更換一個(gè)Bundle時(shí),就把Bundle連同類加載器一起換掉以實(shí)現(xiàn)代碼的熱替換。
在OSGi環(huán)境下,類加載器不再是雙親委派模型中的樹狀結(jié)構(gòu),而是進(jìn)一步發(fā)展為更加復(fù)雜的網(wǎng)狀結(jié)構(gòu)。
當(dāng)收到類加載的請求時(shí),OSGi將按照下面順序進(jìn)行類搜索:
將以java.*開頭的類為派給父類加載器架子啊
否則,將 委派列表名單內(nèi)的類 委派給 父類加載器 加載
否則,將Import列表中的類 委派給Export這個(gè)類的Bundle的類加載器加載
否則,查找當(dāng)前Bundle的ClassPath,使用自己的類加載器加載
否則,查找類是否在自己的Fragment Bundle中,如果在,則委派給Fragment Bundle的類加載器加載
否則,查找Dynamic Import列表的Bundle,委派給對應(yīng)的Bundle的類加載器架子啊
否則,類查找失敗
總結(jié)先大概了解類加載的過程
在了解類加載器是什么東西
然后在了解雙親委派模型
最后實(shí)際就是為熱部署做鋪墊,了解到都是為需求而變化,并未強(qiáng)制使用某種規(guī)范。從3次雙親委派模型的破壞,我們可以看出這個(gè)模型并不是很成熟。
OSGi中對類加載器的使用很值得學(xué)習(xí),弄懂了OSGi的實(shí)現(xiàn),就可以算是掌握了類加載器的精髓。
參考
《深入理解Java虛擬機(jī)——JVM高級特性與最佳實(shí)踐》 周志明 機(jī)械工業(yè)出版社
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/70725.html
摘要:類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個(gè)生命周期包括加載驗(yàn)證準(zhǔn)備解析初始化使用和卸載 類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個(gè)生命周期包括:加載(Loading)、驗(yàn)證(verification)、準(zhǔn)備(preparation)、解析(resolution)、初始化(initialization)、使用(using)和卸載(unloading)
摘要:加載階段在類的加載階段,虛擬機(jī)需要完成以下件事情通過一個(gè)類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流。驗(yàn)證階段驗(yàn)證是連接階段的第一步,這一階段的目的是為了確保文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。 注:本篇文章中的內(nèi)容是根據(jù)《深入理解Java虛擬機(jī)--JVM高級特性與最佳實(shí)踐》而總結(jié)的,如有理解錯(cuò)誤,歡迎大家指正! 虛擬機(jī)把描述類的數(shù)據(jù)從Class文件...
摘要:實(shí)現(xiàn)這個(gè)口號的就是可以運(yùn)行在不同平臺(tái)上的虛擬機(jī)和與平臺(tái)無關(guān)的字節(jié)碼。類加載過程加載加載是類加載的第一個(gè)階段,虛擬機(jī)要完成以下三個(gè)過程通過類的全限定名獲取定義此類的二進(jìn)制字節(jié)流。驗(yàn)證目的是確保文件字節(jié)流信息符合虛擬機(jī)的要求。 引言 我們知道java代碼編譯后生成的是字節(jié)碼,那虛擬機(jī)是如何加載這些class字節(jié)碼文件的呢?加載之后又是如何進(jìn)行方法調(diào)用的呢? 一 類文件結(jié)構(gòu) 無關(guān)性基石 ja...
摘要:最終形成可以被虛擬機(jī)最直接使用的類型的過程就是虛擬機(jī)的類加載機(jī)制。即重寫一個(gè)類加載器的方法驗(yàn)證驗(yàn)證是連接階段的第一步,這一階段的目的是為了確保文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。 《深入理解Java虛擬機(jī):JVM高級特性與最佳實(shí)踐(第二版》讀書筆記與常見相關(guān)面試題總結(jié) 本節(jié)常見面試題(推薦帶著問題閱讀,問題答案在文中都有提到): 簡單說說類加載過...
摘要:二驗(yàn)證驗(yàn)證主要是為了確保文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)的自身安全。五初始化類的初始化階段是類加載過程的最后一步,該階段才真正開始執(zhí)行類中定義的程序代碼或者說是字節(jié)碼。 關(guān)注我,每天三分鐘,帶你輕松掌握一個(gè)Java相關(guān)知識(shí)點(diǎn)。 虛擬機(jī)(JVM)經(jīng)常出現(xiàn)在我們面試中,但是工作中卻很少遇到,導(dǎo)致很多同學(xué)沒有去了解過。其實(shí)除了應(yīng)付面試,作為java程序員,了解...
閱讀 1856·2021-09-28 09:46
閱讀 3172·2019-08-30 14:22
閱讀 1903·2019-08-26 13:36
閱讀 3370·2019-08-26 11:32
閱讀 2130·2019-08-23 16:56
閱讀 1190·2019-08-23 16:09
閱讀 1331·2019-08-23 12:55
閱讀 2174·2019-08-23 11:44