上一篇博文Java類的生命週期概要性地介紹了一個類從「出生」到「凋亡」的各個生命階段。今天,筆者就跟你們聊聊其中很是重要的一個環節:類的加載過程。Java虛擬機中類加載的過程具體包含加載、驗證、準備、解析和初始化這5個階段,各階段涉及的細節較多,在上一篇博文中都有簡短介紹,本文將主要介紹加載階段,其中包括加載方式、加載時機、類加載器原理、實例分析等內容。java
在具體介紹類加載機制以前,咱們先來看下網上關於理解類加載機制的經典題目:spring
public class Singleton { private static Singleton singleton = new Singleton(); public static int counter1; public static int counter2 = 0; private Singleton() { counter1++; counter2++; } public static Singleton getSingleton() { return singleton; } } // 打印靜態屬性的值 public class TestSingleton{ public static void main(String[] args) { Singleton singleton = Singleton.getSingleton(); System.out.println("counter1=" + singleton.counter1); System.out.println("counter2=" + singleton.counter2); } } // 輸出結果: >>> counter1=1 >>> counter2=0
關於爲何counter2=0
,這裏就不具體解釋了,只是想說下它考覈了那幾個點:編程
- 類加載過程的5個階段前後順序:準備階段在初始化以前
- 準備階段和初始化階段各自作的事情
- 靜態初始化的細節:前後順序
言歸正傳,咱們先從類加載的定義提及,一句話概述,緩存
虛擬機將class文件中的二進制數據流加載到JVM運行時數據區的方法區內,並進行驗證、準備、解析和初始化等動做後,在內存中建立java.lang.class對象,做爲對方法區中該類數據結構的訪問入口。tomcat
這裏有幾點要解釋下,class文件是指符合class文件格式的二進制數據流,也就是咱們常說的字節碼文件,它是咱們與JVM約定的格式協議,只要是符合class文件格式的二進制流,均可被JVM加載,這也是JVM跨平臺的基礎;另外,java.lang.class對象只是說在內存建立,並無明確規定是否在Java堆中,對於Hotspot虛擬機,是存放在方法區的。安全
類的加載方式分爲兩種:隱式加載和顯式加載。服務器
實際就是不用咱們代碼主動聲明,而是JVM在適當的時機自動加載類。好比主動引用某個類時,會自動觸發類加載和初始化階段。數據結構
則一般是指經過代碼的方式顯式加載指定類,常見如下幾種:架構
經過
Class.forName()
加載指定類。對於forName(String className)
方法,默認會執行靜態初始化,但若是使用另外一個重載函數forName(String name, boolean initialize, ClassLoader loader)
,其實是能夠經過initialize
來控制是否執行靜態初始化併發經過
ClassLoader.loadClass()
加載指定類,這種方式僅僅是將.class加載到JVM,並不會執行靜態初始化塊,這個等後面談到類加載器的職責時會再強調這一點
關於Class.forName()
是否執行靜態初始化,經過源碼就能一目瞭然:
public static Class<?> forName(String className) // 執行初始化,由於initialize爲true throws ClassNotFoundException { Class<?> caller = Reflection.getCallerClass(); return forName0(className, true, ClassLoader.getClassLoader(caller), caller); } ... public static Class<?> forName(String name, boolean initialize, ClassLoader loader) // 可控的,經過initialize來指定初始化與否 throws ClassNotFoundException { ... return forName0(name, initialize, loader, caller); }
類加載的第一個階段——加載階段具體何時開始,虛擬機規範並未指明,由具體的虛擬機實現決定,可分爲預加載和運行時加載兩種時機:
JAVA_HOME/lib
下的rt.jar,它包含了咱們最經常使用的class,如java.lang.*
、java.util.*
等,在虛擬機啓動時會提早加載,這樣用到時就省去了加載耗時,能加快訪問速度。先上代碼(JDK1.7源碼):
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; } }
loadClass
是類加載機制中最核心的代碼,這段代碼基本闡述瞭如下最核心的兩點:
findLoadedClass(name)
,首先第一步就檢查這個name
表示的類是否已被某個類加載器實例加載過,若已加載,則直接返回已加載的c
,不然才繼續下面的委派等邏輯。即JVM層會對已加載的類進行緩存,那具體是怎麼緩存的的呢?在JVM實現中,有個相似於HashTable
的數據結構叫作SystemDictionary,對已加載的類信息進行緩存,緩存的key
是類加載器實例+類的全限定名,value
則是指向表示類信息的klass數據結構。這也是爲何咱們常說,JVM中加載類的類加載器實例加上類的全限定名,這二者一塊兒才能惟一肯定一個類。每一個類加載器實例就至關因而一個獨立的類命名空間,對於兩個不一樣類加載器實例加載的類,即使名稱相同,也是兩個徹底不一樣的類。
對於新加載的類,緩存沒命中後走雙親委派邏輯——當parent
存在時,會先委派給parent
進行loadClass
,而後parent.loadClass
內部又會進行一樣的向上委派,直至parent
爲null
,委派給根加載器。也就是說委派請求會一直向上傳遞,直到頂層的引導類加載器,而後再統一用ClassNotFoundException異常的方式逐層向下回傳,直到某一層classLoader
在其搜索範圍內找到並加載該類;當parent
不存在時,即沒有父類加載器,此時直接委派給頂層加載器——BootstrapClassLoader
。
從這裏能夠看到雙親委派結構中,類加載器之間的父子層級關係並非經過繼承來實現,而是經過組合的方式即子類加載器持有parent
代理以指向父類加載器來實現的。
ucp
屬性瞭解下~仍是給個定義吧:
經過一個類的全限定名來獲取描述此類的二進制字節流的代碼模塊
經典的三層加載器結構:
一、啓動類加載器(或稱爲引導類加載器):只負責加載<JAVA_HOME>/lib
目錄中的,或是啓動參數-Xbootclasspath
所指定路徑中的特定名稱類庫。該加載器由C++實現,對Java程序不可見,對於自定義加載器,如果未指定parent,則會委派該加載器進行加載。
二、擴展類加載器:負責加載<JAVA_HOME>/lib/ext
目錄中的,或是java.ext.dirs
系統變量所指定的路徑下全部類庫。該加載器由sum.misc.Launcher$ExtClassLoader
實現,可直接使用。
三、應用程序類加載器(或稱爲系統類加載器):負責加載用戶類路徑ClassPath
中全部類庫。該加載器由sum.misc.Launcher$AppClassLoader
實現,可由ClassLoader.getSystemClassLoader()
方法得到。
ExtClassLoader
和AppClassLoader
都是繼承自URLClassLoader
,各自負責的加載路徑都是保存在ucp
屬性中,這個看源碼就能得知。
雙親委派並非一個強制性約束模型,畢竟它自身也有侷限性。不管是歷史代碼層面、SPI設計問題、仍是新的熱部署需求,都不可避免地會違背該原則,累計有三次「破壞」。
經過ClassLoader的源碼可知,雙親委派的實現細節都在loadClass方法中,而該方法是一個protected的,意味着子類能夠覆蓋該方法,從而可繞過雙親委派邏輯。雙親委派模型是在JDK1.2以後才被引入,在此以前的JDK1.0,已有部分用戶經過繼承ClassLoader重寫了loadClass邏輯,這使得後面引入的雙親委派邏輯在這些用戶程序中不起做用。
爲了向前兼容,ClassLoader新增了findClass方法,提倡用戶將本身的類加載邏輯放入findClass中,而不要再去覆蓋loadClass方法。
雙親委派的層次優先級就決定了用戶代碼和JDK基礎類之間的不對等性,即只能用戶代碼調用基礎類,反之不行。對於SPI之類的設計,好比已經成爲Java標準服務的JNDI,其接口代碼是在基礎類中,而具體的實現代碼則是在用戶Classpath下,在雙親委派的限制下,JNDI沒法調用實現層代碼。
開個後門——引入線程上下文類加載器(Thread Context ClassLoader),該加載器可經過java.lang.Thread.setContextClassLoader()
進行設置,若建立線程時未設置,則從父線程繼承;若應用程序的全局範圍都未設置過,則默認設置爲應用程序類加載器,這個可在Launcher的源碼中找到答案。
有了這個,JNDI服務就可以使用該加載器去加載所需的SPI代碼。其餘相似的SPI設計也是這種方式,如JDBC、JCE、JAXB、JBI等。
模塊化熱部署,在生產環境中顯得尤其有吸引力,就像咱們的計算機外設同樣,不用重啓,可隨時更換鼠標、U盤等。 OSGi已經成爲業界事實上的Java模塊化標準,此時類加載器再也不是雙親委派中的樹狀層次,而是複雜的網狀結構。
一般Web服務器須要解決幾個基本問題:
爲了應對以上基本問題,主流的Java Web服務器都會提供多個Classpath存放類庫。對於Tomcat,其目錄結構劃分爲如下4組:
/common
目錄,存放的類庫被Tomcat和全部的Web應用程序共享。/server
目錄,僅被Tomcat使用,其餘Web應用程序不可見。/shared
目錄,可被全部Web應用程序共享,但對Tomcat不可見。/WebApp/WEB-INF
目錄,僅被所屬的Web應用程序使用,對Tomcat和其餘Web應用程序不可見。跟以上目錄對應的,是Tomcat經典的雙親委派類加載器架構:
上圖中,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebAppClassLoader分別負責/common/*
、/server/*
、/shared/*
和/WebApp/WEB-INF/*
目錄下的Java類庫加載,其中WebApp類加載器和Jsp類加載器一般會存在多個實例,每個Web應用程序對應一個WebAppClassLoader,每個JSP文件對應一個Jsp類加載器。
OSGi(Open Service Gateway Initiative)是OSGi聯盟制定的一個基於Java語言的動態模塊化規範,其最著名的應用案例就是Eclipse IDE,它是Eclipse強大插件體系的基礎。
OSGi中的每一個模塊稱爲Bundle,一個Bundle能夠聲明它所依賴的Java Package(經過Import-Package描述),也能夠聲明它容許導出發佈的Java Package(經過Export-Package描述)。Bundle之間的依賴關係爲平級依賴,Bundle類加載器之間只有規則,沒有固定的委派關係。假設存在BundleA、BundleB和BundleC,
BundleA:聲明發布了packageA,依賴了java.*的包 BundleB:聲明依賴了packageA和packageC,同時也依賴了java.*的包 BundleC:聲明發布了packageC,依賴了packageA
一個簡單的OSGi類加載器架構示例以下:
上圖的這種網狀架構帶來了更好的靈活性,但同時也可能產生許多新的隱患。好比Bundle之間的循環依賴,在高併發場景下致使加載死鎖。
本文以一個關於類加載的編程題爲切入點,闡述了類加載階段的具體細節,包括加載方式、加載時機、加載原理,以及雙親委派的優劣點。並以具體的類加載器實例Tomcat和OSGi爲例,簡單分析了類加載器在實踐過程當中的多種選擇。
同步更新到原文。