包括如下 7 個階段:java
其中解析過程在某些狀況下能夠在初始化階段以後再開始,這是爲了支持 Java 的動態綁定。git
虛擬機規範中並無強制約束什麼時候進行加載,可是規範嚴格規定了有且只有下列五種狀況必須對類進行初始化(加載、驗證、準備都會隨着發生):程序員
遇到 new、getstatic、putstatic、invokestatic 這四條字節碼指令時,若是類沒有進行過初始化,則必須先觸發其初始化。最多見的生成這 4 條指令的場景是:使用 new 關鍵字實例化對象的時候;讀取或設置一個類的靜態字段(被 final 修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候;以及調用一個類的靜態方法的時候。github
使用 java.lang.reflect 包的方法對類進行反射調用的時候,若是類沒有進行初始化,則須要先觸發其初始化。數據庫
當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化。數組
當虛擬機啓動時,用戶須要指定一個要執行的主類(包含 main() 方法的那個類),虛擬機會先初始化這個主類;安全
當使用 JDK 1.7 的動態語言支持時,若是一個 java.lang.invoke.MethodHandle 實例最後的解析結果爲 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化;服務器
以上 5 種場景中的行爲稱爲對一個類進行主動引用。除此以外,全部引用類的方式都不會觸發初始化,稱爲被動引用。被動引用的常見例子包括:網絡
System.out.println(SubClass.value); // value 字段在 SuperClass 中定義
SuperClass[] sca = new SuperClass[10];
System.out.println(ConstClass.HELLOWORLD);
包含了加載、驗證、準備、解析和初始化這 5 個階段。多線程
加載是類加載的一個階段,注意不要混淆。
加載過程完成如下三件事:
其中二進制字節流能夠從如下方式中獲取:
確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。
類變量是被 static 修飾的變量,準備階段爲類變量分配內存並設置初始值,使用的是方法區的內存。
實例變量不會在這階段分配內存,它將會在對象實例化時隨着對象一塊兒分配在 Java 堆中。(實例化不是類加載的一個過程,類加載發生在全部實例化操做以前,而且類加載只進行一次,實例化能夠進行屢次)
初始值通常爲 0 值,例以下面的類變量 value 被初始化爲 0 而不是 123。
public static int value = 123;
若是類變量是常量,那麼會按照表達式來進行初始化,而不是賦值爲 0。
public static final int value = 123;
將常量池的符號引用替換爲直接引用的過程。
初始化階段才真正開始執行類中的定義的 Java 程序代碼。初始化階段即虛擬機執行類構造器 <clinit>() 方法的過程。
在準備階段,類變量已經賦過一次系統要求的初始值,而在初始化階段,根據程序員經過程序制定的主觀計劃去初始化類變量和其它資源。
<clinit>() 方法具備如下特色:
public class Test { static { i = 0; // 給變量賦值能夠正常編譯經過 System.out.print(i); // 這句編譯器會提示「非法向前引用」 } static int i = 1; }
與類的構造函數(或者說實例構造器 <init>())不一樣,不須要顯式的調用父類的構造器。虛擬機會自動保證在子類的 <clinit>() 方法運行以前,父類的 <clinit>() 方法已經執行結束。所以虛擬機中第一個執行 <clinit>() 方法的類確定爲 java.lang.Object。
因爲父類的 <clinit>() 方法先執行,也就意味着父類中定義的靜態語句塊要優於子類的變量賦值操做。例如如下代碼:
static 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); // 輸出結果是父類中的靜態變量 A 的值,也就是 2。 }
<clinit>() 方法對於類或接口不是必須的,若是一個類中不包含靜態語句塊,也沒有對類變量的賦值操做,編譯器能夠不爲該類生成 <clinit>() 方法。
接口中不可使用靜態語句塊,但仍然有類變量初始化的賦值操做,所以接口與類同樣都會生成 <clinit>() 方法。但接口與類不一樣的是,執行接口的 <clinit>() 方法不須要先執行父接口的 <clinit>() 方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也同樣不會執行接口的 <clinit>() 方法。
虛擬機會保證一個類的 <clinit>() 方法在多線程環境下被正確的加鎖和同步,若是多個線程同時初始化一個類,只會有一個線程執行這個類的 <clinit>() 方法,其它線程都會阻塞等待,直到活動線程執行 <clinit>() 方法完畢。若是在一個類的 <clinit>() 方法中有耗時的操做,就可能形成多個線程阻塞,在實際過程當中此種阻塞很隱蔽。
實現類的加載動做。在 Java 虛擬機外部實現,以便讓應用程序本身決定如何去獲取所須要的類。
兩個類相等:類自己相等,而且使用同一個類加載器進行加載。這是由於每個類加載器都擁有一個獨立的類名稱空間。
這裏的相等,包括類的 Class 對象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回結果爲 true,也包括使用 instanceof 關鍵字作對象所屬關係斷定結果爲 true。
從 Java 虛擬機的角度來說,只存在如下兩種不一樣的類加載器:
啓動類加載器(Bootstrap ClassLoader),這個類加載器用 C++ 實現,是虛擬機自身的一部分;
全部其餘類的加載器,這些類由 Java 實現,獨立於虛擬機外部,而且全都繼承自抽象類 java.lang.ClassLoader。
從 Java 開發人員的角度看,類加載器能夠劃分得更細緻一些:
啓動類加載器(Bootstrap ClassLoader)此類加載器負責將存放在 <JAVA_HOME>\lib 目錄中的,或者被 -Xbootclasspath 參數所指定的路徑中的,而且是虛擬機識別的(僅按照文件名識別,如 rt.jar,名字不符合的類庫即便放在 lib 目錄中也不會被加載)類庫加載到虛擬機內存中。啓動類加載器沒法被 Java 程序直接引用,用戶在編寫自定義類加載器時,若是須要把加載請求委派給啓動類加載器,直接使用 null 代替便可。
擴展類加載器(Extension ClassLoader)這個類加載器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它負責將 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系統變量所指定路徑中的全部類庫加載到內存中,開發者能夠直接使用擴展類加載器。
應用程序類加載器(Application ClassLoader)這個類加載器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。因爲這個類加載器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,所以通常稱爲系統類加載器。它負責加載用戶類路徑(ClassPath)上所指定的類庫,開發者能夠直接使用這個類加載器,若是應用程序中沒有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。
應用程序都是由三種類加載器相互配合進行加載的,若是有必要,還能夠加入本身定義的類加載器。
下圖展現的類加載器之間的層次關係,稱爲類加載器的雙親委派模型(Parents Delegation Model)。該模型要求除了頂層的啓動類加載器外,其他的類加載器都應有本身的父類加載器。這裏類加載器之間的父子關係通常經過組合(Composition)關係來實現,而不是經過繼承(Inheritance)的關係實現。
(一)工做過程
一個類加載器首先將類加載請求傳送到父類加載器,只有當父類加載器沒法完成類加載請求時才嘗試加載。
(二)好處
使得 Java 類隨着它的類加載器一塊兒具備一種帶有優先級的層次關係,從而是的基礎類獲得統一。
例如 java.lang.Object 存放在 rt.jar 中,若是編寫另一個 java.lang.Object 的類並放到 ClassPath 中,程序能夠編譯經過。由於雙親委派模型的存在,因此在 rt.jar 中的 Object 比在 ClassPath 中的 Object 優先級更高,由於 rt.jar 中的 Object 使用的是啓動類加載器,而 ClassPath 中的 Object 使用的是應用程序類加載器。正由於 rt.jar 中的 Object 優先級更高,由於程序中全部的 Object 都是這個 Object。
(三)實現
如下是抽象類 java.lang.ClassLoader 的代碼片斷,其中的 loadClass() 方法運行過程以下:先檢查類是否已經加載過,若是沒有則讓父類加載器去加載。當父類加載器加載失敗時拋出 ClassNotFoundException,此時嘗試本身去加載。
public abstract class ClassLoader { // The parent class loader for delegation private final ClassLoader parent; public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } 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) { 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; } } protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); } }
FileSystemClassLoader 是自定義類加載器,繼承自 java.lang.ClassLoader,用於加載文件系統上的類。它首先根據類的全名在文件系統上查找類的字節代碼文件(.class 文件),而後讀取該文件內容,最後經過 defineClass() 方法來把這些字節代碼轉換成 java.lang.Class 類的實例。
java.lang.ClassLoader 類的方法 loadClass() 實現了雙親委派模型的邏輯,所以自定義類加載器通常不去重寫它,而是經過重寫 findClass() 方法。
public class FileSystemClassLoader extends ClassLoader { private String rootDir; public FileSystemClassLoader(String rootDir) { this.rootDir = rootDir; } protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } private byte[] getClassData(String className) { String path = classNameToPath(className); try { InputStream ins = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bufferSize = 4096; byte[] buffer = new byte[bufferSize]; int bytesNumRead; while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } private String classNameToPath(String className) { return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; } }
免費Java資料須要本身領取,涵蓋了Java、Redis、MongoDB、MySQL、Zookeeper、Spring Cloud、Dubbo高併發分佈式等教程,一共30G。
傳送門: https://mp.weixin.qq.com/s/JzddfH-7yNudmkjT0IRL8Q