一個類在使用前,如何經過類調用靜態字段,靜態方法,或者new一個實例對象,第一步就是須要類加載,而後是鏈接和初始化,最後才能使用。java
類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialzation)、使用(Using)和卸載(Unloading)7 個階段。其中驗證、準備、解析 3 個部分統稱爲鏈接(Linking),這 7 個階段的發生順序以下圖所示:web
加載、驗證、準備、初始化和卸載這 5 個階段的順序是肯定的,類的加載過程必須按照這種順序循序漸進地開始,而解析階段則不必定:它在某些狀況下能夠在初始化階段以後再開始,這是爲了支持 Java 語言的運行時綁定(也稱爲動態綁定或晚期綁定)。注意,這裏筆者寫的是循序漸進地 「開始」,而不是循序漸進地 「進行」 或 「完成」,強調這點是由於這些階段一般都是互相交叉地混合式進行的,一般會在一個階段執行的過程當中調用、激活另一個階段。面試
什麼狀況下須要開始類加載過程的第一個階段:加載?Java 虛擬機規範中並無進行強制約束,這點能夠交給虛擬機的具體實現來自由把握。可是對於初始化階段,虛擬機規範則是嚴格規定了有且只有 5 種狀況必須當即對類進行 「初始化」(而加載、驗證、準備天然須要在此以前開始):shell
對於這 5 種會觸發類進行初始化的場景,虛擬機規範中使用了一個很強烈的限定語:「有且只有」,這 5 種場景中的行爲稱爲對一個類進行主動引用。除此以外,全部引用類的方式都不會觸發初始化,稱爲被動引用。apache
類加載器就是將 .java 代碼文件編譯成 .class 字節碼文件後,Java虛擬機的類加載器經過讀取此類的二進制流,轉換成目標類的實例。安全
除了Java會生成字節碼外,運行在JVM上的JRuby,Scala,Groovy一樣須要編譯成對應的 .class 文件,這裏列舉了四種不一樣的字節碼,不單是Java才生成字節碼文件。多線程
經常使用的類加載器有4種:app
1.Bootstrap ClassLoader:啓動類加載器,加載JAVA_HOME/lib
目錄下的類。以下圖選中的就是框架
2.ExtClassLoader:擴張類加載器,加載JAVA_HOME/lib/ext
目錄下的類。eclipse
3.AppClassLoader:應用程序類加載器,加載用戶指定的classpath(存放 src 目錄 Java 文件編譯以後的 class 文件和 xml、properties 等資源配置文件的 src/main/webapp/WEB-INF/classes 目錄)下的類
4.UserClassLoader:用戶自定義的類加載器(只要繼承 ClassLoader並實現 findClass(String name) 方法),自定義加載路徑。
類加載時並不須要等到某個類被首次主動使用時再加載它,JVM類加載器會在預料某個類要使用時預先加載。雙親委派模型,以下圖:
Java 類加載基於雙親委派模型——當有類加載請求時,從下往上檢查類是否被加載,若是沒被加載,UserClassLoader 就委託父類 AppClassLoader 加載,AppClassLoader 繼續委託其父類 ExtClassLoader 加載,接着分派給 Bootstrap ClasssLoader 加載;
若是沒法加載就返回到發起加載請求的類加載一直到由最開始發起加載請求的 UserClassLoader 加載,全部類最終都會去到頂層。Bootstrap ClasssLoader 開始加載,沒法加載就返回子加載器處理,一直到最開始的加載器。
這樣子,就算用戶自定義了 java.lang.Object 類和系統的 java.lang.Object 類重複,也不會被加載,下面咱們就來自定義本身的類加載器。
/** * Created by cong on 2018/8/2. */ public class MyClassLoader extends ClassLoader { public MyClassLoader() { super(); } public MyClassLoader(ClassLoader parent) { super(parent); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { // do something // 本身先不加載,先讓父類加載 return super.findClass(name); } public static void main(String[] args) throws ClassNotFoundException { MyClassLoader myLoader = new MyClassLoader(); // 打印當前類路徑 System.out.println(System.getProperty("java.class.path")); // ClassPath路徑下並不存在Demo.class類,故拋出異常 System.out.println(myLoader.loadClass("Demo").getClassLoader().getClass().getName()); } }
運行結果以下:
學習自定義類加載器後,咱們看下源碼裏雙親委派模型是怎麼加載類的。源碼以下:
public abstract class ClassLoader { private final ClassLoader parent; protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 先檢查類是否已經被加載 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異常被拋出則代表父類加載器加載失敗 } if (c == null) { // 若是父類沒法加載,就本身加載 long t1 = System.nanoTime(); c = findClass(name); 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 類裏有同步代碼塊 synchronized (getClassLoadingLock(name)),而在 JDK1.6 以前是方法 protected synchronized Class<?> loadClass(String name, boolean resolve)
上鎖,鎖住方法當前對象。
這就致使一個問題,當 A 包依賴 B 包,A 在本身的類加載器的 loadClass 方法中,最終調用到 B 的類加載器的 loadClass 方法。A 先鎖住本身的類加載器,而後去申請 B 的類加載器的鎖,當 B 也依賴 A 包時,B 加載 A 的包時,過程相反,在多線程下,就容易產生死鎖。若是類加載器是單線程運行就會安全,但效率會很低 同步代碼塊 synchronized (getClassLoadingLock(name)) 鎖住的是一個特定對象。
private final ConcurrentHashMap<String, Object> parallelLockMap; protected Object getClassLoadingLock(String className) { Object lock = this; // parallelLockMap是一個ConcurrentHashMap if (parallelLockMap != null) { // 鎖對象 Object newLock = new Object(); // putIfAbsent(K, V)方法查看K(className)和V(newLock)是否相互對應, // 是的就返回V(newLock),不然返回null // 每一個className關聯一個鎖,並將這個鎖返回,縮小了鎖定粒度了,只要類名不一樣,就會匹配不一樣的鎖, // 就是並行加載,相似ConcurrentHashMap裏面的分段鎖, // 不鎖住整個Map,而是鎖住一個Segment,每次只須要對Segment上鎖或解鎖,以空間換時間 lock = parallelLockMap.putIfAbsent(className, newLock); if (lock == null) { // 建立一個新鎖對象 lock = newLock; } } return lock;
經過並行加載,能夠提高加載效率,而後講下類加載的面試題,在 Java 反射中 Class.forName() 加載類和使用 ClassLoader 加載類是不同的。例子以下:
/** * Created by cong on 2018/8/5. */ public class MyCase { static { System.out.println("執行了靜態代碼塊"); } private static String field = methodCheck(); public static String methodCheck() { System.out.println("執行了靜態代方法"); return "給靜態變量賦值"; } }
----------------------------------------------------
/** * Created by cong on 2018/8/5. */ public class DemoTest { public static void main(String[] args) { try { System.out.println("Class.forName開始執行:"); //hjc是包名 Class.forName("hjc.MyCase"); System.out.println("ClassLoader開始執行:"); ClassLoader.getSystemClassLoader().loadClass("hjc.MyCase"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
運行結果以下:
Class.forName 是加載 MyCase 類並完成初始化,給靜態代碼塊和靜態變量賦值,而 ClassLoader 只是將類加載進 JVM 虛擬機,並無初始化。
接下來咱們進入Class.forName的源碼探究,源碼以下:
@CallerSensitive public static Class<?> forName(String className) throws ClassNotFoundException { return forName0(className, true, ClassLoader.getClassLoader(Reflection.getCallerClass())); }
Class.forName 底層也是調用了 ClassLoader,只是第二個參數爲 true,即加載類並初始化,默認就會初始化類,JDBC 鏈接就是用 Class.forName 加載驅動。因此註冊鏈接驅動會在靜態代碼塊執行,Sprng 裏的 IOC 是經過 ClassLoader 來產生,能夠控制 Bean 的延遲加載(首次使用才建立)。
爲了實現代碼熱替換,模塊化和動態化,就像鼠標同樣即插即用,雙親委派這種樹狀的加載器就難以勝任,因而出現了 OSGI 加載模型,OSGI 裏每一個程序模塊(Bundle,就是普通的 jar 包, 只是加入了特殊的頭信息,是最小的部署模塊)都會有本身的類加載器,當須要更換程序時,就連同 Bundle 和類加載器一塊兒替換,是一種網狀的加載模型,Bundle 間互相委託加載,並非層次化的。
Java 類加載機制的隔離是經過不一樣類加載器加載指定目錄來實現的,類加載的共享機制是經過雙親委派模型來實現,而 OSGI 實現隔離靠的是每一個 Bundle 都自帶一個獨立的類加載器 ClassLoader。
OSGI 加載 Bundle 模塊的順序
若是用 Java 的結構的項目去部署,當項目複雜度提高時,每次上線,代碼只是增長或者修改了部分功能,但都得關掉服務,從新部署全部的代碼和配置,管理溝通成本都很高,很容產生線上事故,而 OSGI 的應用是一個模塊化的系統,避免了部署時 jar 或 classpath 錯綜複雜依賴管理,發佈應用和更新應用都很強大,能夠熱替換特定的 Bundle 模塊,提升部署可靠性。
接下來咱們用IDE建立一個OSGI應用,首先要去 http://download.eclipse.org/equinox/ 下載最新的OSGI 框架enquinox
建立一個 OSGI 應用。打開 Eclipse,File->New->Project:
選擇 OSGI 框架 Equniox(Eclipse 強大的插件機制就是構建於 OSGI Bundle 之上,Eclipse 自己就包含了 Equniox) :
接下來,勾選建立 Activator 類,新建一個創Activator 類,每一個 Bundle 啓動時都會調用 Bundle(模塊)裏 Activator(類)的 start 方法,中止時調用 stop 方法,代碼以下:
import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; /** * Created by cong on 2018/8/5. */ public class Activator implements BundleActivator { private static BundleContext context; static BundleContext getContext() { return context; } public void start(BundleContext bundleContext) throws Exception { Activator.context = bundleContext; //添加輸出This is OSGI Projcect System.out.println("This is OSGI Projcect"); } public void stop(BundleContext bundleContext) throws Exception { Activator.context = null; } }
接下來進行一下配置,Run->Run Configuration-> 雙擊 OSGI Framework 生成項目配置:以下圖:
而後點擊運行按鈕,能夠看到控制檯輸出 This is OSGI Projcect。在控制檯咱們輸入 ss ( short status) 查看服務狀態:
This is OSGI Projcect osgi> ss "Framework is launched." id State Bundle 0 ACTIVE org.eclipse.osgi_3.12.100.v20180210-1608 1 ACTIVE org.apache.felix.gogo.runtime_0.10.0.v201209301036 2 ACTIVE org.apache.felix.gogo.command_0.10.0.v201209301215 // ACTIVE代表 com.osgi.bundle.demo Bundle運行中 3 ACTIVE com.osgi.bundle.demo_1.0.0.qualifier 4 ACTIVE org.apache.felix.gogo.shell_0.10.0.v201212101605 5 ACTIVE org.eclipse.equinox.console_1.1.300.v20170512-2111 // 中止 com.osgi.bundle.demo Bundle osgi> stop com.osgi.bundle.demo osgi> ss "Framework is launched." id State Bundle 0 ACTIVE org.eclipse.osgi_3.12.100.v20180210-1608 1 ACTIVE org.apache.felix.gogo.runtime_0.10.0.v201209301036 2 ACTIVE org.apache.felix.gogo.command_0.10.0.v201209301215 // RESOLVED 代表 Bundle com.osgi.bundle.demo 中止了 3 RESOLVED com.osgi.bundle.demo_1.0.0.qualifier 4 ACTIVE org.apache.felix.gogo.shell_0.10.0.v201212101605 5 ACTIVE org.eclipse.equinox.console_1.1.300.v20170512-2111 // 經過close關閉整個應用框架 osgi> close Really want to stop Equinox? (y/n; default=y) y osgi>
一個 Bundle 包含 MANIFEST.MF,也就是 Bundle 的頭信息,Java 代碼以及配置文件(XML,Properties),其中 MANIFEST.MF 包含了下面的信息。以下所示:
/*版本號*/ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 /*名字*/ Bundle-Name: Demo Bundle-SymbolicName: com.osgi.bundle.demo Bundle-Version: 1.0.0.qualifier /*Bundle類*/ Bundle-Activator: com.osgi.bundle.demo.Activator Bundle-Vendor: OSGI /*依賴環境*/ Bundle-RequiredExecutionEnvironment: JavaSE-1.7 /*導入的包*/ Import-Package: org.osgi.framework;version="1.3.0" Bundle-ActivationPolicy: lazy
1.控制框架
1.launch 啓動框架
2.shutdown 中止框架
3.close 關閉、退出框架
4.exit 當即退出,至關於 System.exit
5.init 卸載全部 bundle(前提是已經 shutdown)
6.setprop 設置屬性,在運行時進行
2.控制 Bundle
1.Install 安裝 uninstall 卸載
2.Stop 中止
3.Refresh 刷新
4.Update 更新
3.展現狀態
1.Status 展現安裝的 bundle 和註冊的服務
2.Ss 展現全部 bundle 的簡單狀態
3.Services 展現註冊服務的詳細信息
4.Packages 展現導入、導出包的狀態
5.Bundles 展現全部已經安裝的 bundles 的狀態
6.Headers 展現 bundles 的頭信息,即 MANIFEST.MF 中的內容
7.Log 展現 LOG 入口信息
4.其餘
Exec 在另一個進程中執行一個命令(阻塞狀態)
1.Fork 和 EXEC 不一樣的是不會引發阻塞
2.Gc 促使垃圾回收
3.Getprop 獲得屬性,或者某個屬性
5.控制啓動級別
1.Sl 獲得某個 bundle 或者整個框架的 start level 信息
2.Setfwsl 設置框架的 start level
3.Setbsl 設置 bundle 的 start level
4.setibsl 設置初始化 bundle 的 start level