摘要:虛擬機為了保證一個類的方法在多線程環境中被正確地加鎖同步。但啟動類加載器不可能認識這些代碼。實現模塊化熱部署的關鍵則是它的自定義類加載器機制的實現。
概念區分:
加載、類加載、類加載器
類加載是一個過程。
加載(Loading)是類加載這一個過程的階段。
類加載器是ClassLoader類或其子類。
本文中的”類“的描述都包括了類和接口的可能性,因為每個Class文件都有可能代表Java語言中的一個類或接口。
本文中的”Class文件“并非特指存在于具體磁盤中的文件,更準確理解應該是一串二進制的字節流。
類加載過程分為:
加載 Loading(注意,別與類加載混淆,類加載是個過程,加載是其一個階段)
驗證 Verification
準備 Preparation
解析 Resolution
初始化 Initialization
加載在這個階段,主要完成3件事:
通過一個類的全限定名來獲取定義此類的二進制字節流。 不一定要從本地的Class文件獲取,可以從jar包,網絡,甚至十六進制編輯器弄出來的。開發人員可以重寫類加載器的loadClass()方法。
將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構
在內存中生產一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口
驗證這一階段目的為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。
文件格式驗證,如魔數(0xCAFEBABE)開頭、主次版本號是否在當前虛擬機處理范圍之內等。
元數據驗證,此階段開始就不是直接操作字節流,而是讀取方法區里的信息,元數據驗證大概就是驗證是否符合Java語言規范
字節碼驗證,是整個驗證過程中最復雜的一個階段,主要目的是通過數據流和控制流分析,確定程序語義是否合法,符合邏輯。JDK6之后做了優化,不在驗證,可以通過-XX:-UseSplitVerifier關閉優化。
符號引用驗證,此階段可以看做是類自己身意外的信息進行匹配性校驗。
準備此階段正是為 類變量 分配內存和設置 類變量 初始值的階段。這些變量所使用的內存都將在方法區中進行分配。注意這里僅包括 類變量(被static修飾的變量),而不是包括實例變量。
public static int value = 123;
在這個階段中,value的值是0
以下是基本數據類型的零值
數據類型 | 零值 | 數據類型 | 零值 |
---|---|---|---|
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;
編譯時javac將會為value生產ConstantValue屬性,在準備階段虛擬機會根據ConstatnValue的設置,將value賦值為123;。
解析這個階段有點復雜,我還講不清,先跳過。 //TODO 2017年10月29日
初始化類初始化是類加載過程的最后一步。在前面的類加載過程中,除了在加載階段,用戶應用程序可以通過自己定義類加載參與之外,其余動作完全由虛擬機主導和控制。
到了這個初始化階段,才真正開始執行類中定義的Java程序代碼(或者說是字節碼)。
在編譯時,編譯器或自動收集 類 中的所有類變量(被static修飾的變量)的賦值操作和靜態語句塊中的語句合并,從而生成出一個叫
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 }
虛擬機為了保證一個類的
重頭戲來了,了解上面的類加載過程之后,我們對類加載有個感性的認識,于是我們可以使用類加載器去決定如何去獲取所需的類。
雖然類加載器僅僅實現類的加載動作(階段),但它在Java程序中起到的作用遠遠不限于類加載階段。
對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在Java虛擬機中的唯一性。
也就是說,判斷兩個類是否”相等“(這個“相等”包括類的Class對象的equals()方法、isAssignableForm()方法、isInstance()方法的返回結果,也包括使用instanceof關鍵字做對象所屬關系的判定),只有在兩個類是由同一個類加載器加載的前提下才有意義,否則,即使這兩個類來源于同一個Class文件,被同一個虛擬機加載,只要它們的類加載器不一樣,那么這兩個類就必定不同。
package com.jc.jvm.classloader; import java.io.IOException; import java.io.InputStream; /** * 類加載器與instanceof關鍵字例子 * */ 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的角度來講,只存在兩種不同的類加載器:
啟動類加載器(Bootstrap ClassLoader),這個類加載器是使用C++語言實現,是虛擬機自身的一部分。
另一種就是其他的類加載器,這些類加載器都由Java語言實現,獨立于虛擬機外部,并且都繼承自抽象類java.lang.ClassLoader
而從Java開發人員的角度來看,類加載器還可以劃分得跟細致些:
啟動類加載器(Bootstrap ClassLoader): 這個類加載器負責將存放在$JAVA_HOME/lib目錄下的,并且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即使放在lib目錄下也不會被加載)類庫加載到虛擬機內存中。可以被-Xbootclasspath參數修改。啟動類加載器無法被Java程序直接引用。
擴展類加載器(Extension ClassLoader):這個加載器由sun.misc.Lancher$ExtClassLoader實現,負責加載$JAVA_HOME/lib/ext目錄下的,或者被java.ext.dirs系統變量指定的路徑中的所有類庫。開發者可以直接使用擴展類加載器。
應用程序加載器(Application ClassLoader):這個類加載器由sum.misc.Launcher$AppClassLoader實現。由于這個類加載器是ClassLoader的getSystemClassLoader()方法的返回值,所以一般也稱它為 系統類加載器。如果應用程序中沒有自定義過自己的類加載器,則使用該類加載器作為默認。它負責加載用戶類路徑(ClassPath)上所指定的類庫。
再加上自定義類加載器,那么它們之間的層次關系為:雙親委托模型(Parents Delegation Model)。雙親委托模型要求除了頂層的啟動類加載器外,其余的類加載器都應當有自己的父類加載器。這里類加載器之間的父子關系一般不會以集成繼承(Inheritance)的關系來實現,而是都使用組合(Composition)關系來服用父加載器的代碼。
類加載的雙親委派模型實在JDK1.2期間引入的,但它不是一個強制性的約束模型,而是Java設計者推薦給開發者的一種類加載器實現方式。
雙親委派模型的工作過程是:如果一個類加載器收到類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此。因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。
雙親委派模型的實現:
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; } }雙親委派模型的破壞
由于雙親委派模型不是一個強制性的約束模型,而是Java設計者推薦給開發者的類加載器實現方式。因為歷史原因和需求不同于是出現過3次破壞:
由于java.lang.ClassLoader在JDK1.0就已經存在,而用戶去繼承ClassLoader,就是為覆寫loadClass()方法,而這個方法實現有雙親委派模型的邏輯。于是這樣被覆蓋,雙親委派模型就被打破了。于是Java設計者在JDK1.2給ClassLoader添加一個新的方法findClass(),提倡大家應當把自己的類加載邏輯寫到findClass()方法中,這樣就不會破壞雙親委派模型的規則。因為loadClass()方法的邏輯里就是如果父類加載失敗,則會調用自己的findClass()來完成加載,請看上面雙親委派模型的實現。
雙親委派很好地解決了各個類加載器的基礎類的統一問題,但如果是基礎類,但啟動類加載器不認得怎么辦。 如JNDI服務,JNDI在JDK1.3開始就作為平臺服務,它的代碼是由啟動類加載器加載(JDK1.3時放進去的rt.jar),但JNDI的目的就是對資源進行集中管理和查找,它需要調用由獨立廠商實現并不熟在應用的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)代碼。但啟動類加載器不可能”認識“這些代碼。
于是Java設計團隊引入一個不太優雅的設計:就是線程上下文類加載器(Thread Context ClassLoader),這個類加載器可以設置,但默認是就是應用程序類加載器。有了這個 線程上下文類加載器(這名字有點長) 后,就可以做一些”舞弊“的事情(我喜歡稱為hack),JNDI服務使用這個線程上下類加載器去加載所需要的SPI代碼,也就是父類加載器請求子類加載其去完成類加載的動作。 于是又一次違背了雙親委派模型。詳情請參考: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(); //這個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; } } ); }
這次破壞就嚴重咯,是由于用戶對程序動態性的追求而導致的。也就是:代碼替換(HotSwap)、模塊熱部署(Hot Deployment)等。
對于模塊化之爭有,Sun公司的Jigsaw項目和OSGi組織的規范。
目前來看OSGi語句成為了業界的Java模塊化標準。
OSGi實現模塊化熱部署的關鍵則是它的自定義類加載器機制的實現。每一個程序模塊(OSGi中成為Bundle)都有一個自己的類加載器。 當需要更換一個Bundle時,就把Bundle連同類加載器一起換掉以實現代碼的熱替換。
在OSGi環境下,類加載器不再是雙親委派模型中的樹狀結構,而是進一步發展為更加復雜的網狀結構。
當收到類加載的請求時,OSGi將按照下面順序進行類搜索:
將以java.*開頭的類為派給父類加載器架子啊
否則,將 委派列表名單內的類 委派給 父類加載器 加載
否則,將Import列表中的類 委派給Export這個類的Bundle的類加載器加載
否則,查找當前Bundle的ClassPath,使用自己的類加載器加載
否則,查找類是否在自己的Fragment Bundle中,如果在,則委派給Fragment Bundle的類加載器加載
否則,查找Dynamic Import列表的Bundle,委派給對應的Bundle的類加載器架子啊
否則,類查找失敗
總結先大概了解類加載的過程
在了解類加載器是什么東西
然后在了解雙親委派模型
最后實際就是為熱部署做鋪墊,了解到都是為需求而變化,并未強制使用某種規范。從3次雙親委派模型的破壞,我們可以看出這個模型并不是很成熟。
OSGi中對類加載器的使用很值得學習,弄懂了OSGi的實現,就可以算是掌握了類加載器的精髓。
參考
《深入理解Java虛擬機——JVM高級特性與最佳實踐》 周志明 機械工業出版社
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/70725.html
摘要:類從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期包括加載驗證準備解析初始化使用和卸載 類從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期包括:加載(Loading)、驗證(verification)、準備(preparation)、解析(resolution)、初始化(initialization)、使用(using)和卸載(unloading)
摘要:實現這個口號的就是可以運行在不同平臺上的虛擬機和與平臺無關的字節碼。類加載過程加載加載是類加載的第一個階段,虛擬機要完成以下三個過程通過類的全限定名獲取定義此類的二進制字節流。驗證目的是確保文件字節流信息符合虛擬機的要求。 引言 我們知道java代碼編譯后生成的是字節碼,那虛擬機是如何加載這些class字節碼文件的呢?加載之后又是如何進行方法調用的呢? 一 類文件結構 無關性基石 ja...
摘要:最終形成可以被虛擬機最直接使用的類型的過程就是虛擬機的類加載機制。即重寫一個類加載器的方法驗證驗證是連接階段的第一步,這一階段的目的是為了確保文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。 《深入理解Java虛擬機:JVM高級特性與最佳實踐(第二版》讀書筆記與常見相關面試題總結 本節常見面試題(推薦帶著問題閱讀,問題答案在文中都有提到): 簡單說說類加載過...
摘要:二驗證驗證主要是為了確保文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機的自身安全。五初始化類的初始化階段是類加載過程的最后一步,該階段才真正開始執行類中定義的程序代碼或者說是字節碼。 關注我,每天三分鐘,帶你輕松掌握一個Java相關知識點。 虛擬機(JVM)經常出現在我們面試中,但是工作中卻很少遇到,導致很多同學沒有去了解過。其實除了應付面試,作為java程序員,了解...
閱讀 1823·2021-09-28 09:46
閱讀 3143·2019-08-30 14:22
閱讀 1878·2019-08-26 13:36
閱讀 3343·2019-08-26 11:32
閱讀 2081·2019-08-23 16:56
閱讀 1151·2019-08-23 16:09
閱讀 1303·2019-08-23 12:55
閱讀 2148·2019-08-23 11:44