本文目的:html
虛擬機把描述類的數據從 Class 文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的 Java 類型,這就是虛擬機的類加載機制。java
在Java語言裏面,類型的加載、鏈接和初始化過程都是在程序運行期間完成的數組
類的個生命週期以下圖:安全
爲支持運行時綁定,解析過程在某些狀況下可在初始化以後再開始,除解析過程外的其餘加載過程必須按照如圖順序開始。網絡
驗證是鏈接階段的第一步,這一階段的目的是爲了確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。數據結構
能夠考慮使用 -Xverify:none
參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。多線程
爲類變量分配內存並設置類變量初始值,這些變量所使用的內存都將在方法區中進行分配。app
虛擬機將常量池內的符號引用替換爲直接引用的過程。
解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符 7 類符號引用進行。ide
到初始化階段,才真正開始執行類中定義的 Java 程序代碼,此階段是執行 <clinit>()
方法的過程。模塊化
<clinit>()
方法是由編譯器按語句在源文件中出現的順序,依次自動收集類中的全部類變量的賦值動做和靜態代碼塊中的語句合併產生的。(不包括構造器中的語句。構造器是初始化對象的,類加載完成後,建立對象時候將調用的 <init>()
方法來初始化對象)
靜態語句塊中只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句塊能夠賦值,可是不能訪問,以下程序:
public class Test { static { // 給變量賦值能夠正常編譯經過 i = 0; // 這句編譯器會提示"非法向前引用" System.out.println(i); } static int i = 1; }
<clinit>()
不須要顯式調用父類構造器,虛擬機會保證在子類的 <clinit>()
方法執行以前,父類的 <clinit>()
方法已經執行完畢,也就意味着父類中定義的靜態語句塊要優先於子類的變量賦值操做。
<clinit>()
方法對於類或接口來講並非必需的,若是一個類中沒有靜態語句塊,也沒有對變量的賦值操做,那麼編譯器能夠不爲這個類生成 <clinit>()
方法。
虛擬機會保證一個類的 <clinit>()
方法在多線程環境中被正確地加鎖、同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的 <clinit>()
方法,其餘線程都須要阻塞等待,直到活動線程執行 <clinit>()
方法完畢。
對於初始化階段,虛擬機規範規定了有且只有 5 種狀況必須當即對類進行「初始化」(而加載、驗證、準備天然須要在此以前開始):
- 當使用 JDK 1.7 的動態語言支持時,若是一個 java.lang.invoke.MethodHandle 實例最後的解析結果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化。
第5種狀況,我暫時看不懂。
以上這 5 種場景中的行爲稱爲對一個類進行主動引用。除此以外,全部引用類的方式都不會觸發初始化,稱爲被動引用,例如:
MyClass[] cs = new MyClass[10];
把實現類加載階段中的「經過一個類的全限定名來獲取描述此類的二進制字節流」這個動做的代碼模塊稱爲「類加載器」。
將 class 文件二進制數據放入方法區內,而後在堆內(heap)建立一個 java.lang.Class 對象,Class 對象封裝了類在方法區內的數據結構,而且向開發者提供了訪問方法區內的數據結構的接口。
目前類加載器卻在類層次劃分、OSGi、熱部署、代碼加密等領域很是重要,咱們運行任何一個 Java 程序都會涉及到類加載器。
對於任意一個類,都須要由加載它的類加載器和這個類自己一同確立其在Java虛擬機中的惟一性。
即便兩個類來源於同一個 Class 文件,被同一個虛擬機加載,只要加載它們的類加載器不一樣,那這兩個類也不相等。
這裏所指的「相等」,包括表明類的 Class 對象的 equals() 方法、 isAssignableFrom() 方法、isInstance() 方法的返回結果,也包括使用 instanceof 關鍵字作對象所屬關係斷定等狀況。
若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每個層次的類加載器都是如此,所以全部的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父加載器反饋本身沒法完成這個加載請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試本身去加載。
這裏類加載器之間的父子關係通常不會以繼承(Inheritance)的關係來實現,而是都使用組合(Composition)關係來複用父加載器的代碼。
Bootstrap 類加載器是用 C++ 實現的,是虛擬機自身的一部分,若是獲取它的對象,將會返回 null;擴展類加載器和應用類加載器是獨立於虛擬機外部,爲 Java 語言實現的,均繼承自抽象類 java.lang.ClassLoader ,開發者可直接使用這兩個類加載器。
Application 類加載器對象能夠由 ClassLoader.getSystemClassLoader()
方法的返回,因此通常也稱它爲系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。
雙親委派模型對於保證 Java 程序的穩定運做很重要,例如類 java.lang.Object
,它存放在 rt.jar 之中,不管哪個類加載器要加載這個類,最終都是委派給處於模型最頂端的啓動類加載器進行加載,所以 Object 類在程序的各類類加載器環境中都是同一個類。
雙親委派模型的加載類邏輯可參考以下代碼:
// 代碼摘自《深刻理解Java虛擬機》 protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 首先,檢查請求的類是否已經被加載過了 Class c = findLoadedClass(name); if (c == null) { try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // 若是父類加載器拋出ClassNotFoundException // 說明父類加載器沒法完成加載請求 } if (c == null) { // 在父類加載器沒法加載的時候 // 再調用自己的findClass方法來進行類加載 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; }
雙親委派模型主要出現過 3 較大規模的「被破壞」狀況。
1) 雙親委派模型在引入以前已經存在破壞它的代碼存在了。
雙親委派模型在 JDK 1.2 以後才被引入,而類加載器和抽象類 java.lang.ClassLoader
則在 JDK 1.0 時代就已經存在,JDK 1.2以後,其添加了一個新的 protected 方法 findClass()
,在此以前,用戶去繼承 ClassLoader 類的惟一目的就是爲了重寫 loadClass()
方法,而雙親委派的具體邏輯就實如今這個方法之中,JDK 1.2 以後已不提倡用戶再去覆蓋 loadClass()
方法,而應當把本身的類加載邏輯寫到 findClass()
方法中,這樣就能夠保證新寫出來的類加載器是符合雙親委派規則的。
2) 基礎類沒法調用類加載器加載用戶提供的代碼。
雙親委派很好地解決了各個類加載器的基礎類的統一問題(越基礎的類由越上層的加載器進行加載),但若是基礎類又要調用用戶的代碼,例如 JNDI 服務,JNDI 如今已是 Java 的標準服務,它的代碼由啓動類加載器去加載(在 JDK 1.3 時放進去的 rt.jar ),但 JNDI 的目的就是對資源進行集中管理和查找,它須要調用由獨立廠商實現並部署在應用程序的 ClassPath 下的 JNDI 接口提供者(SPI,Service Provider Interface,例如 JDBC 驅動就是由 MySQL 等接口提供者提供的)的代碼,但啓動類加載器只能加載基礎類,沒法加載用戶類。
爲此 Java 引入了線程上下文類加載器(Thread Context ClassLoader)。這個類加載器能夠經過
java.lang.Thread.setContextClassLoaser()
方法進行設置,若是建立線程時還未設置,它將會從父線程中繼承一個,若是在應用程序的全局範圍內都沒有設置過的話,那這個類加載器默認就是應用程序類加載器。
如此,JNDI 服務使用這個線程上下文類加載器去加載所須要的 SPI 代碼,也就是父類加載器請求子類加載器去完成類加載的動做,這種行爲實際上就是打通了雙親委派模型的層次結構來逆向使用類加載器,實際上已經違背了雙親委派模型的通常性原則,但這也是迫不得已的事情。Java 中全部涉及 SPI 的加載動做基本上都採用這種方式,例如 JNDI、JDBC、JCE、JAXB 和 JBI 等。
3) 用戶對程序動態性的追求。
代碼熱替換(HotSwap)、模塊熱部署(Hot Deployment)等,OSGi 實現模塊化熱部署的關鍵則是它自定義的類加載器機制的實現。每個程序模塊(Bundle)都有一個本身的類加載器,當須要更換一個 Bundle 時,就把 Bundle 連同類加載器一塊兒換掉以實現代碼的熱替換。
在 OSGi 環境下,類加載器再也不是雙親委派模型中的樹狀結構,而是進一步發展爲更加複雜的網狀結構,當收到類加載請求時,OSGi 將按照下面的順序進行類搜索:
1)將以 java.* 開頭的類委派給父類加載器加載。
2)不然,將委派列表名單內的類委派給父類加載器加載。
3)不然,將 Import 列表中的類委派給 Export 這個類的 Bundle 的類加載器加載。
4)不然,查找當前 Bundle 的 ClassPath,使用本身的類加載器加載。
5)不然,查找類是否在本身的 Fragment Bundle 中,若是在,則委派給 Fragment Bundle 的類加載器加載。
6)不然,查找 Dynamic Import 列表的 Bundle,委派給對應 Bundle 的類加載器加載。
7)不然,類查找失敗。
上面的查找順序中只有開頭兩點仍然符合雙親委派規則,其他的類查找都是在平級的類加載器中進行的。OSGi 的 Bundle 類加載器之間只有規則,沒有固定的委派關係。
Java 默認 ClassLoader,只加載指定目錄下的 class,若是須要動態加載類到內存,例如要從遠程網絡下來類的二進制,而後調用這個類中的方法實現個人業務邏輯,如此,就須要自定義 ClassLoader。
自定義類加載器分爲兩步:
針對第 1 步,爲何要繼承 ClassLoader 這個抽象類,而不繼承 AppClassLoader 呢?
由於它和 ExtClassLoader 都是 Launcher 的靜態內部類,其訪問權限是缺省的包訪問權限。
static class AppClassLoader extends URLClassLoader{...}
第 2 步,JDK 的 loadCalss()
方法在全部父類加載器沒法加載的時候,會調用自己的 findClass()
方法來進行類加載,所以咱們只需重寫 findClass()
方法找到類的二進制數據便可。
下面我自定義了一個簡單的類加載器,並加載一個簡單的類。
首先是須要被加載的簡單類:
// 存放於D盤根目錄 public class Test { public static void main(String[] args) { System.out.println("Test類已成功加載運行!"); ClassLoader classLoader = Test.class.getClassLoader(); System.out.println("加載個人classLoader:" + classLoader); System.out.println("classLoader.parent:" + classLoader.getParent()); } }
並使用 javac -encoding utf8 Test.java
編譯成 Test.class 文件。
類加載器代碼以下:
import java.io.*; public class MyClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { // 加載D盤根目錄下指定類名的class String clzDir = "D:\\" + File.separatorChar + name.replace('.', File.separatorChar) + ".class"; byte[] classData = getClassData(clzDir); if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } private byte[] getClassData(String path) { try (InputStream ins = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream() ) { int bufferSize = 4096; byte[] buffer = new byte[bufferSize]; int bytesNumRead = 0; while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } }
使用類加載器加載調用 Test 類:
public class MyClassLoaderTest { public static void main(String[] args) throws Exception { // 指定類加載器加載調用 MyClassLoader classLoader = new MyClassLoader(); classLoader.loadClass("Test").getMethod("test").invoke(null); } }
輸出信息:
Test.test()已成功加載運行! 加載個人classLoader:class MyClassLoader classLoader.parent:class sun.misc.Launcher$AppClassLoader
如上所說,爲解決基礎類沒法調用類加載器加載用戶提供代碼的問題,Java 引入了線程上下文類加載器(Thread Context ClassLoader)。這個類加載器默認就是 Application 類加載器,而且能夠經過 java.lang.Thread.setContextClassLoaser()
方法進行設置。
// Now create the class loader to use to launch the application try { loader = AppClassLoader.getAppClassLoader(extcl); } catch (IOException e) { throw new InternalError( "Could not create application class loader" ); } // Also set the context class loader for the primordial thread. Thread.currentThread().setContextClassLoader(loader);
那麼問題來了,咱們使用 ClassLoader.getSystemClassLoader()
方法也能夠獲取到 Application 類加載器,使用它就能夠加載用戶類了呀,爲何還須要線程上下文類加載器?
其實直接使用 getSystemClassLoader()
方法獲取 AppClassLoader 加載類也能夠知足一些狀況,但有時候咱們須要使用自定義類加載器去加載某個位置的類時,例如Tomcat 使用的線程上下文類加載器並不是 AppClassLoader ,而是 Tomcat 自定義類加載器。
以 Tomcat 爲例,其每一個 Web 應用都有一個對應的類加載器實例,該類加載器使用代理模式,首先嚐試去加載某個類,若是找不到再代理給父類加載器這與通常類加載器的順序是相反的。
這是 Java Servlet 規範中的推薦作法,其目的是使得 Web 應用本身的類的優先級高於 Web 容器提供的類。
更多關於 Tomcat 類加載器的知識,這裏暫時先不講了。
最後,推薦與感謝:
深刻理解Java虛擬機(第2版)
碼出高效:Java開發手冊
java new一個對象的過程當中發生了什麼 - 天風的文章 - 知乎
深刻探討類加載器
Class.forName()用法詳解
真正理解線程上下文類加載器(多案例分析)