@java
前面學習了虛擬機的內存結構、對象的分配和建立,但對象所對應的類是怎麼加載到虛擬機中來的呢?加載過程當中須要作些什麼?什麼是雙親委派機制以及爲何要打破雙親委派機制?mysql
類的生命週期包含了如上的7個階段,其中驗證、準備、解析統稱爲鏈接 ,類的加載主要是前五個階段,每一個階段基本上保持如上順序開始(僅僅是開始,實際上執行是交叉混合的),只有解析階段不必定,在初始化後也有可能纔開始執行解析,這是爲了支持動態語言。sql
加載就是將字節碼的二進制流轉化爲方法區的運行時數據結構,並生成類所對象的Class對象,字節碼二進制流能夠是咱們編譯後的class文件,也能夠從網絡中獲取,或者運行時動態生成(動態代理)等等。
那何時會觸發類加載呢?這個在虛擬機規範中沒有明肯定義,只是規定了什麼時候須要執行初始化(稍後詳細分析)。數組
這個階段很好理解,就是進行必要的校驗,確保加載到內存中的字節碼是符合要求的,主要包含如下四個校驗步驟(瞭解便可):tomcat
該階段是爲類變量(static)分配內存並設置零值,即類只要通過準備階段其中的靜態變量就是可以使用的了,但此時類變量的值還不是咱們想要的值,須要通過初始化階段纔會將咱們但願的值賦值給對應的靜態變量。安全
解析就是將常量池中的符號引用替換爲直接引用的過程。符號引用就是一個代號,好比咱們的名字,而這裏能夠理解爲就是類的徹底限定名;直接引用則是對應的具體的人、物,這裏就是指目標的內存地址。爲何須要符號引用呢?由於類在加載到內存以前尚未分配內存地址,所以必然須要一個東西指代它。這個階段包含了類或接口的解析、字段解析、類方法解析、接口方法解析,在解析的過程當中可能會拋出如下異常:網絡
這是類加載過程當中的最後一個步驟,主要是收集類的靜態變量的賦值動做和static塊中的語句合成<cinit>方法,經過該方法根據咱們的意願爲靜態變量賦值以及執行static塊,該方法會被加鎖,確保多線程狀況下只有一個線程能初始化成功,利用該特性能夠實現單例模式。虛擬機規定了有且只有遇到如下狀況時必須先確保對應類的初始化完成(加載、準備必然在此以前):數據結構
下面分析幾個案例代碼,讀者們能夠先思考後再運行代碼看看和本身想的是否同樣。多線程
先定義以下兩個類:app
public class SuperClazz { static { System.out.println("SuperClass init!"); } public static int value=123; public static final String HELLOWORLD="hello world"; public static final int WHAT = value; } public class SubClaszz extends SuperClazz { static{ System.out.println("SubClass init!"); } }
而後進行下面的調用:
public class Initialization { public static void main(String[]args){ Initialization initialization = new Initialization(); initialization.M1(); } public void M1(){ System.out.println(SubClaszz.value); } }
第一個案例是經過子類去引用父類中的靜態變量,兩個類都會加載和初始化麼?打印結果看看:
SuperClass init! 123
能夠看到只有父類初始化了,那麼父類必然是加載了的,問題就在於子類有沒有被加載呢?能夠加上參數:-XX:+TraceClassLoading再執行(該參數的做用就是打印被加載了的類),能夠看到子類是被加載了的。因此經過子類引用父類靜態變量,父子類都會被加載,但只有父類會進行初始化。
爲何呢?反編譯後能夠看到生成了以下指令:
0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 3: getstatic #6 // Field ex7/init/SubClaszz.value:I 6: invokevirtual #7 // Method java/io/PrintStream.println:(I)V 9: return
關鍵就是getstatic指令就會觸發類的初始化,可是爲何子類不會初始化呢?由於這個變量是來自於父類的,爲了提升效率,因此虛擬機進行了優化,這種狀況只須要初始化父類就好了。
調用下面的方法:
public void M2(){ SubClaszz[]sca = new SubClaszz[10]; }
執行後能夠發現,使用數組,不會觸發初始化,但父子類都會被加載。
public void M3(){ System.out.println(SuperClazz.HELLOWORLD); }
引用常量不會觸發類的加載和初始化,由於常量在編譯後就已經存在當前class的常量池。
public void M4(){ System.out.println(SubClaszz.WHAT); }
經過常量去引用其它的靜態變量會發生什麼呢?這個和案例一結果是同樣的。
在咱們平時開發中,肯定一個類須要經過徹底限定名,而不能簡單的經過名字,由於在不一樣的路徑下咱們是能夠定義同名的類的。那麼在虛擬機中又是怎麼區分類的呢?在虛擬機中須要類加載器+徹底限定名一塊兒來指定一個類的惟一性,即相同限定名的類若由兩個不一樣的類加載器加載,那虛擬機就不會把它們當作一個類。從這裏咱們能夠看出類加載器必定是有多個的,那麼不一樣的類加載器是怎麼組織的?它們又分別須要加載哪些類呢?
從虛擬角度看,只有兩種類型的類加載器:啓動類加載器(BootstrapClassLoader)和非啓動類加載器。前者是C++實現,屬於虛擬機的一部分,後者則是由Java實現的,獨立於虛擬機的外部,而且所有繼承自抽象類java.lang.ClassLoader。
但從Java自己來看,一直保持着三層類加載器、雙親委派的結構,固然除了Java自己提供的三層類加載器,咱們還能夠自定義實現類加載器。如上圖,上面三個就是原生的類加載器,每個都是下一個類加載器的父加載器,注意這裏都是採用組合而非繼承。當開始加載類時,首先交給父加載器加載,父加載器加載了子加載器就不用再加載了,而如果父加載器加載不了,就會交給子加載器加載,這就是雙親委派機制。這就比如工做中遇到了沒法處理的事,你會去請示直接領導,直接領導處理不了,再找上層領導,而後上層領導以爲這是個小事,不用他親自動手,就讓你的直接領導去作,接着他又交給你去作等等。下面來看看每一個類加載器的具體做用:
經過這三個類加載以及雙親委派機制,一個顯而易見的好處就是,不一樣的類隨它的類加載器自然具備了加載優先級,像Object、String等等這些核心類庫天然就會在咱們的應用程序類以前被加載,使得程序更安全,不會出現錯誤,Spring的父子容器也是這樣的一個設計。經過下面這段代碼能夠看到每一個類所對應的類加載器:
public class ClassLoader { public static void main(String[] args) { System.out.println(String.class.getClassLoader()); //啓動類加載器 System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader());//拓展類加載器 System.out.println(ClassLoader.class.getClassLoader());//應用程序類加載器 } }
輸出:
null sun.misc.Launcher$ExtClassLoader@4b67cf4d sun.misc.Launcher$AppClassLoader@14dad5dc
剛剛我舉了工做中的一個例子來講明雙親委派機制,但現實中咱們不須要事事都去請示領導,一樣類加載器也不是徹底遵循雙親委派機制,在必要的時候是能夠打破這個規則的。下面列舉四個破壞的狀況,在此以前咱們須要先了解下雙親 委派的代碼實現原理,在java.lang.ClassLoader類中有一個loadClass以及findClass方法:
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. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } } protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
從上面能夠看到首先是調用parent去加載類,沒有加載到才調用自身的findClass方法去加載。也就是說用戶在實現自定義類加載器的時候須要覆蓋的是fiindClass而不是loadClass,這樣才能知足雙親委派模型。
下面具體來看看破壞雙親委派的幾個場景。
第一次破壞是在雙親委派模型出現以前, 由於該模型是在JDK1.2以後才引入的,那麼在此以前,抽象類java.lang.ClassLoader就已經存在了,用戶自定義的類加載器都會去覆蓋該類中的loadClass方法,因此雙親委派模型出現後,就沒法避免用戶覆蓋該方法,所以新增了findClass引導用戶去覆蓋該方法實現本身的類加載邏輯。
第二次破壞是因爲這個模型自己缺陷致使的,由於該模型保證了類的加載優先級,可是有些接口是Java定義在覈心類庫中,但具體的服務實現是由用戶提供的,這時候就不得不破壞該模型才能實現,典型的就是Java中的SPI機制(對SPI不瞭解的讀者能夠翻閱我以前的文章或是其它資料,這裏不進行闡述)。J
DBC的驅動加載就是SPI實現的,因此直接看到java.sql.DriverManager類,該類中有一個靜態初始化塊:
static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } private static void loadInitialDrivers() { String drivers; try { drivers = AccessController.doPrivileged(new PrivilegedAction<String>() { public String run() { return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; } AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); try{ while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); println("DriverManager.initialize: jdbc.drivers = " + drivers); if (drivers == null || drivers.equals("")) { return; } String[] driversList = drivers.split(":"); println("number of Drivers:" + driversList.length); for (String aDriver : driversList) { try { println("DriverManager.Initialize: loading " + aDriver); Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) { println("DriverManager.Initialize: load failed: " + ex); } } }
主要看ServiceLoader.load方法,這個就是經過SPI去加載咱們引入java.sql.Driver實現類(好比引入mysql的驅動包就是com.mysql.cj.jdbc.Driver):
public static <S> ServiceLoader<S> load(Class<S> service) { ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }
這個方法主要是從當前線程中獲取類加載器,而後經過這個類加載器去加載驅動實現類(這個叫線程上下文類加載器,咱們也可使用這個技巧去打破雙親委派),那這裏會獲取到哪個類加載器呢?具體的設置是在sun.misc.Launcher類的構造器中:
public Launcher() { Launcher.ExtClassLoader var1; try { var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } try { this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } Thread.currentThread().setContextClassLoader(this.loader); String var2 = System.getProperty("java.security.manager"); if (var2 != null) { SecurityManager var3 = null; if (!"".equals(var2) && !"default".equals(var2)) { try { var3 = (SecurityManager)this.loader.loadClass(var2).newInstance(); } catch (IllegalAccessException var5) { } catch (InstantiationException var6) { } catch (ClassNotFoundException var7) { } catch (ClassCastException var8) { } } else { var3 = new SecurityManager(); } if (var3 == null) { throw new InternalError("Could not create SecurityManager: " + var2); } System.setSecurityManager(var3); } }
能夠看到設置的就是AppClassLoader。你可能會有點疑惑,這個類加載器加載類的時候不也是先調用父類加載器加載麼,怎麼就打破雙親委派了呢?其實打破雙親委派指的就是類的層次結構,延伸意思就是類的加載優先級,這裏本應該是在加載核心類庫的時候卻提早將咱們應用程序中的類庫給加載到虛擬機中來了。
上圖是Tomcat類加載的類圖,前面三個不用說,CommonClassLoader、CatalinaClassLoader、SharedClassLoader、WebAppClassLoader、JspClassLoader則是Tomcat本身實現的類加載器,分別加載common包、server包、shared包、WebApp/WEB-INF/lib包以及JSP文件,前面三個在tomcat 6以後已經合併到根目錄下的lib目錄下。而WebAppClassLoader則是每個應用程序對應一個,JspClassLoader是每個JSP文件都會對應一個,而且這兩個類加載器都沒有父類加載器,這也就違背了雙親委派模型。
爲何每一個應用程序須要單獨的WebAppClassLoader實例?由於每一個應用程序須要彼此隔離,假如在兩個應用中定義了同樣的類(徹底限定名),若是遵循雙親委派那就只會存在一份了,另外不一樣的應用還有可能依賴同一個類庫的不一樣版本,這也須要隔離,因此每個應用程序都會對應一個WebAppClassLoader,它們共享的類庫可讓SharedClassLoader加載,另外這些類加載加載的類對Tomcat自己來講也是隔離的(CatalinaClassLoader加載的)。
爲何每一個JSP文件須要對應單獨的一個JspClassLoader實例?這是因爲JSP是支持運行時修改的,修改後會丟棄掉以前編譯生成的class,並從新生成一個JspClassLoader實例去加載新的class。
以上就是Tomcat爲何要打破雙親委派模型的緣由。
OSGI是用於實現模塊熱部署,像Eclipse的插件系統就是利用OSGI實現的,這個技術很是複雜同時使用的也愈來愈少了,感興趣的讀者可自行查閱資料學習,這裏再也不進行闡述。
類加載的過程讓咱們瞭解到一個類是如何被加載到內存中,須要通過哪些階段;而類加載器和雙親委派模型則是告訴咱們應該怎麼去加載類、類的加載優先級是怎樣的,其中的設計思想咱們也能夠學習借鑑;最後須要深入理解的是爲何須要打破雙親委派,在遇到相應的場景時應該怎麼作。