完整圖以下:java
若是本身想手寫一個 Java 虛擬機的話,主要考慮哪些結構呢?數據庫
類加載器子系統負責從文件系統或者網絡中加載 Class 文件,Class 文件在文件開頭有特定的文件標識(CAFE BABE)。bootstrap
ClassLoader 只負責 Class 文件的加載,至於它是否能夠運行,則由 Execution Engine 決定。數組
加載的類信息存放於一塊稱爲方法區的內存空間。除了類的信息外,方法區中還會存放運行時常量池信息,可能還包括字符串字面量和數字常量(這部分常量信息是 Class 文件中常量池部分的內存映射)安全
例以下面的一段簡單的代碼bash
/** * 類加載子系統 * @author: Nemo */ public class HelloLoader { public static void main(String[] args) { System.out.println("我已經被加載啦"); } }
它的加載過程是怎麼樣的呢?網絡
完整的流程圖以下所示數據結構
經過一個類的全限定名獲取定義此類的二進制字節流多線程
將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構app
在內存中生成一個表明這個類的 java.lang.Class 對象,做爲方法區這個類的各類數據的訪問入口
目的在於確保 Class 文件的字節流中包含信息符合當前虛擬機要求,保證被加載類的正確性,不會危害虛擬機自身安全。
主要包括四種驗證,文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證。
工具:Binary Viewer 查看
若是出現不合法的字節碼文件,那麼將會驗證不經過
同時咱們能夠經過安裝 IDEA 的插件,來查看咱們的 Class 文件
安裝完成後,咱們編譯完一個 class 文件後,點擊 view 便可顯示咱們安裝的插件來查看字節碼方法了
爲類變量分配內存而且設置該類變量的默認初始值,即零值。
這裏不包含用 final 修飾的 static,由於 final 在編譯的時候就會分配了,準備階段會顯式初始化;
這裏不會爲實例變量分配初始化,類變量會分配在方法區中,而實例變量是會隨着對象一塊兒分配到 Java 堆中。
例以下面這段代碼
/** * @author: Nemo */ public class HelloApp { private static int a = 1; // 準備階段爲0,在下個階段,也就是初始化的時候纔是1 public static void main(String[] args) { System.out.println(a); } }
上面的變量 a 在準備階段會賦初始值,但不是 1,而是 0。
將常量池內的符號引用轉換爲直接引用的過程。
事實上,解析操做每每會伴隨着 JVM 在執行完初始化以後再執行。
符號引用就是一組符號來描述所引用的目標。符號引用的字面量形式明肯定義在《Java 虛擬機規範》的 Class 文件格式中。直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。
解析動做主要針對類或接口、字段、類方法、接口方法、方法類型等。對應常量池中的 CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等
初始化階段就是執行類構造器法 <clinit>()
的過程。
此方法不需定義,是 javac 編譯器自動收集類中的全部類變量的賦值動做和靜態代碼塊中的語句合併而來。
也就是說,當咱們代碼中包含 static 變量的時候,就會有 clinit 方法
構造器方法中指令按語句在源文件中出現的順序執行。
<clinit>()
不一樣於類的構造器。(關聯:構造器是虛擬機視角下的 <init>()
)若該類具備父類,JVM 會保證子類的 <clinit>()
執行前,父類的 <clinit>()
已經執行完畢。
任何一個類在聲明後,都有生成一個構造器,默認是空參構造器
/** * @author: Nemo */ public class ClassInitTest { private static int num = 1; static { num = 2; number = 20; System.out.println(num); System.out.println(number); //報錯,非法的前向引用 } private static int number = 10; public static void main(String[] args) { System.out.println(ClassInitTest.num); // 2 System.out.println(ClassInitTest.number); // 10 } }
關於涉及到父類時候的變量賦值過程
/** * @author: Nemo */ public class ClinitTest1 { static class Father { public static int A = 1; static { A = 2; } } static class Son extends Father { public static int b = A; } public static void main(String[] args) { System.out.println(Son.b); } }
咱們輸出結果爲 2,也就是說首先加載 ClinitTest1 的時候,會找到 main 方法,而後執行 Son 的初始化,可是 Son 繼承了 Father,所以還須要執行 Father 的初始化,同時將 A 賦值爲 2。咱們經過反編譯獲得 Father 的加載過程,首先咱們看到原來的值被賦值成 1,而後又被複製成 2,最後返回
iconst_1 putstatic #2 <com/atguigu/java/chapter02/ClinitTest1$Father.A> iconst_2 putstatic #2 <com/atguigu/java/chapter02/ClinitTest1$Father.A> return
虛擬機必須保證一個類的 <clinit>()
方法在多線程下被同步加鎖。
/** * @author: Nemo */ public class DeadThreadTest { public static void main(String[] args) { new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t 線程t1開始"); new DeadThread(); }, "t1").start(); new Thread(() -> { System.out.println(Thread.currentThread().getName() + "\t 線程t2開始"); new DeadThread(); }, "t2").start(); } } class DeadThread { static { if (true) { System.out.println(Thread.currentThread().getName() + "\t 初始化當前類"); while(true) { } } } }
上面的代碼,輸出結果爲
線程t1開始 線程t2開始 線程t2 初始化當前類
從上面能夠看出初始化後,只可以執行一次初始化,這也就是同步加鎖的過程
JVM 支持兩種類型的類加載器 。分別爲引導類加載器(Bootstrap ClassLoader)和自定義類加載器(User-Defined ClassLoader)。
從概念上來說,自定義類加載器通常指的是程序中由開發人員自定義的一類類加載器,可是 Java 虛擬機規範卻沒有這麼定義,而是將全部派生於抽象類 ClassLoader 的類加載器都劃分爲自定義類加載器。
不管類加載器的類型如何劃分,在程序中咱們最多見的類加載器始終只有 3 個,以下所示:
這裏的四者之間是包含關係,不是上層和下層,也不是子系統的繼承關係。
咱們經過一個類,獲取它不一樣的加載器
/** * @author: Nemo */ public class ClassLoaderTest { public static void main(String[] args) { // 獲取系統類加載器 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); System.out.println(systemClassLoader); // 獲取其上層的:擴展類加載器 ClassLoader extClassLoader = systemClassLoader.getParent(); System.out.println(extClassLoader); // 試圖獲取 根加載器 ClassLoader bootstrapClassLoader = extClassLoader.getParent(); System.out.println(bootstrapClassLoader); // 獲取自定義加載器 ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); System.out.println(classLoader); // 獲取String類型的加載器 ClassLoader classLoader1 = String.class.getClassLoader(); System.out.println(classLoader1); } }
獲得的結果,從結果能夠看出 根加載器沒法直接經過代碼獲取,同時目前用戶代碼所使用的加載器爲系統類加載器。同時咱們經過獲取 String 類型的加載器,發現是 null,那麼說明 String 類型是經過根加載器進行加載的,也就是說 Java 的核心類庫都是使用根加載器進行加載的。
sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@1540e19d null sun.misc.Launcher$AppClassLoader@18b4aac2 null
在 Java 的平常應用程序開發中,類的加載幾乎是由上述 3 種類加載器相互配合執行的,在必要時,咱們還能夠自定義類加載器,來定製類的加載方式。
爲何要自定義類加載器?
用戶自定義類加載器實現步驟:
loadClass()
方法,從而實現自定義的類加載類,可是在 JDK1.2 以後已再也不建議用戶去覆蓋 loadclass()
方法,而是建議把自定義的類加載邏輯寫在 findclass()
方法中findclass()
方法及其獲取字節碼流的方式,使自定義類加載器編寫更加簡潔。剛剛咱們經過概念瞭解到了,根加載器只可以加載 java /lib 目錄下的 class,咱們經過下面代碼驗證一下
/** * @author: Nemo */ public class ClassLoaderTest1 { public static void main(String[] args) { System.out.println("*********啓動類加載器************"); // 獲取BootstrapClassLoader 可以加載的API的路徑 URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs(); for (URL url : urls) { System.out.println(url.toExternalForm()); } // 從上面路徑中,隨意選擇一個類,來看看他的類加載器是什麼:獲得的是null,說明是 根加載器 ClassLoader classLoader = Provider.class.getClassLoader(); } }
獲得的結果
*********啓動類加載器************ file:/E:/Software/JDK1.8/Java/jre/lib/resources.jar file:/E:/Software/JDK1.8/Java/jre/lib/rt.jar file:/E:/Software/JDK1.8/Java/jre/lib/sunrsasign.jar file:/E:/Software/JDK1.8/Java/jre/lib/jsse.jar file:/E:/Software/JDK1.8/Java/jre/lib/jce.jar file:/E:/Software/JDK1.8/Java/jre/lib/charsets.jar file:/E:/Software/JDK1.8/Java/jre/lib/jfr.jar file:/E:/Software/JDK1.8/Java/jre/classes null
ClassLoader 類,它是一個抽象類,其後全部的類加載器都繼承自 ClassLoader(不包括啓動類加載器)
方法名稱 | 概述 |
---|---|
getParent() | 返回該類加載器的超類加載器 |
loadClass(Sting name) | 加載名稱爲 name 的類,返回結果爲 java.lang.Class 類的實例 |
findClass(String name) | 查找名稱爲 name 的類,返回結果爲 java.lang.Class 類的實例 |
findLoadedClass(String name) | 查找名稱爲 name 的已經被加載過的類,返回結果爲 java.lang.Class 類的實例 |
defineClass(String name,Byte[b],int off,int len) | 把字節數組 b 中的內容轉換爲一個 Java 類,返回結果爲 java.lang.Class 類的實例 |
resolveClass(Class<?> c | 鏈接指定的一個 Java 類 |
sun.misc.Launcher 它是一個 java 虛擬機的入口應用
獲取 ClassLoader 的途徑
方法一:獲取當前 ClassLoader
clazz.getClassLoader()
通常用 clazz 表示一個類的實例,而 class 只是個關鍵字
Thread.currentThread().getContextClassLoader()
ClassLoader.getSystemClassLoader()
方法四:獲取調用者的 ClassLoader
DriverManager.getCallerClassLoader()
Java 虛擬機對 class 文件採用的是按需加載的方式,也就是說當須要使用該類時纔會將它的 class 文件加載到內存生成 class 對象。並且加載某個類的 class 文件時,Java 虛擬機採用的是雙親委派模式,即把請求交由父類處理,它是一種任務委派模式。
當咱們加載 jdbc.jar 用於實現數據庫鏈接的時候,首先咱們須要知道的是 jdbc.jar 是基於 SPI 接口進行實現的,因此在加載的時候,會進行雙親委派,最終從根加載器中加載 SPI 核心類,而後在加載 SPI 接口類,接着在進行反向委派,經過線程上下文類加載器進行實現類 jdbc.jar 的加載。
經過上面的例子,咱們能夠知道,雙親機制能夠
沙盒(英語:sandbox,又譯爲沙箱),計算機術語,在計算機安全領域中是一種安全機制,爲運行中的程序提供的隔離環境。
自定義 String 類,可是在加載自定義 String 類的時候會率先使用引導類加載器加載,而引導類加載器在加載的過程當中會先加載j dk 自帶的文件(rt.jar 包中 java\lang\String.class),報錯信息說沒有 main 方法,就是由於加載的是 rt.jar 包中的 String 類。這樣能夠保證對 Java 核心源代碼的保護,這就是沙箱安全機制。
在JVM中表示兩個 class 對象是否爲同一個類存在兩個必要條件:
換句話說,在 JVM 中,即便這兩個類對象(class 對象)來源同一個 Class 文件,被同一個虛擬機所加載,但只要加載它們的 ClassLoader 實例對象不一樣,那麼這兩個類對象也是不相等的。
JVM 必須知道一個類型是由啓動加載器加載的仍是由用戶類加載器加載的。若是一個類型是由用戶類加載器加載的,那麼 JVM 會將這個類加載器的一個引用做爲類型信息的一部分保存在方法區中。當解析一個類型到另外一個類型的引用的時候,JVM 須要保證這兩個類型的類加載器是相同的。
Java 程序對類的使用方式分爲:王動使用和被動使用。
主動使用,又分爲七種狀況:
除了以上七種狀況,其餘使用 Java 類的方式都被看做是對類的被動使用,都不會致使類的初始化。