JVM被分爲三個主要的子系統:html
(1)類加載器子系統(2)運行時數據區(3)執行引擎java
(1)類加載子系統負責從文件系統或者網絡中加載class文件,class文件在文件開有特定的文件標識(0xCAFEBABE)。數據庫
(2)類加載器(Class Loader
)只負責class文件的加載,至於它是否能夠運行,則由執行引擎(Execution Engine)決定。bootstrap
(3)加載的類信息存放於一塊稱爲方法區的內存空間。除了類的信息外,方法區中還會存放運行時常量池信息,可能還包括字符串字面量和數字常量(這部分常量信息是Class文件中常量池部分的內存映射)。數組
(4)Class對象是存放在堆區的。安全
假若有一個Car.java
文件,編譯後生成一個Car.class
字節碼文件:bash
一個類型從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期將會經歷加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)七個階段,其中驗證、準備、解析三個部分統稱爲鏈接(Linking)。完整的流程圖以下所示:網絡
加載、驗證、準備、初始化和卸載這五個階段的順序是肯定的。爲了支持Java語言的運行時綁定,解析階段也能夠是在初始化以後進行的。(以上順序流程指的是程序開始的順序,在實際運行中,這些階段一般都是互相交叉地混合進行的,會在一個階段執行的過程當中調用、激活另外一個階段)。數據結構
「加載」(Loading)階段是整個「類加載」(Class Loading)過程當中的一個階段,JVM須要完成三件事:多線程
java.lang.Class
對象,做爲方法區這個類的各類數據的訪問入口。加載class文件的方式
目的在於確保Class文件的字節流中包含信息符合當前虛擬機要求,保證被加載類的正確性,不會危害虛擬機自身安全。
主要包括四種驗證:文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證。
文件格式驗證
第一階段要驗證字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處理。
元數據驗證
第二階段是對字節碼描述的信息進行語義分析。
字節碼驗證
經過數據流分析和控制流分析,肯定程序語義是合法的、符合邏輯的。
符號引用驗證
對類自身之外(常量池中的各類符號引用)的各種信息進行匹配性校驗,通俗來講就是,該類是否缺乏或者被禁止訪問它依賴的某些外部類、方法、字段等資源。
咱們能夠經過安裝IDEA的插件——jclasslib Bytecode viewer
,來查看咱們的Class文件:
安裝完成後,咱們編譯完一個class文件後,點擊View--> Show Bytecode With Jclasslib
便可顯示咱們安裝的插件來查看字節碼。
例以下面這段代碼:
public class Hello { private static int a = 1; // 準備階段爲0,在下個階段,也就是初始化的時候纔是1。 public static void main(String[] args) { System.out.println(a); } }
初始化階段就是執行類構造器法<clinit>()的過程。此方法不需定義,是javac
編譯器自動收集類中的全部類變量的賦值動做和靜態代碼塊(static{}塊)中的語句合併而來,編譯器收集的順序是由語句在源文件中出現的順序決定的。
<clinit>()
方法<clinit>
()不一樣於類的構造器函數。(關聯:構造器函數是虛擬機視角下的<init>
()方法。若該類具備父類,JVM會保證子類的<clinit>()
執行前,父類的<clinit>()
已經執行完畢。所以在Java虛擬機中第一個被執行的<clinit>()
方法的類型確定是java.lang.Object
。
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; // prepare:number = 0--> number-->initial: 20-->10 public static void main(String[] args) { System.out.println(ClassInitTest.num); // 2 System.out.println(ClassInitTest.number); // 10 } }
關於涉及到父類時候的變量賦值過程:
public class ClinitTest { 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) { // 加載Father類,其次加載Son類 System.out.println(Son.B); } }
咱們輸出結果爲 2,也就是說首先加載ClinitTest的時候,會找到main方法,而後執行Son的初始化,可是Son繼承了Father,所以還須要執行Father的初始化,同時將A賦值爲2。咱們經過反編譯獲得Father的加載過程,首先咱們看到原來的值被賦值成1,而後又被複製成2,最後返回:
iconst_1 putstatic #2 <com/kai/jvm/ClinitTest1$Father.A> iconst_2 putstatic #2 <com/kai/jvm/ClinitTest1$Father.A> return
虛擬機必須保證一個類的<clinit>()
方法在多線程下被同步加鎖。
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 初始化當前類
從上面能夠看出只可以執行一次初始化,其中一條線程一直在阻塞等待。
在類加載階段中,實現「經過一個類的全限定名來獲取描述該類的二進制字節流」這個動做的代碼就被稱爲「類加載器」(ClassLoader)。
JVM支持兩種類型的類加載器 ,分別爲啓動類加載器(Bootstrap ClassLoader)和自定義類加載器(User-Defined ClassLoader)。
從概念上來說,自定義類加載器通常指的是程序中由開發人員自定義的一類類加載器,可是Java虛擬機規範卻沒有這麼定義,而是將全部派生於抽象類ClassLoader的類加載器都劃分爲自定義類加載器。
不管類加載器的類型如何劃分,在程序中咱們最多見的類加載器主要有3類,以下所示:
Tips:各種加載器之間的關係不是傳統意義上的繼承關係。
咱們經過一個類,獲取不一樣的加載器:
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@4554617c null sun.misc.Launcher$AppClassLoader@18b4aac2 null
咱們經過下面代碼驗證一下:
public class ClassLoaderTest { 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(); System.out.println(classLoader); } }
獲得的結果(%20是空格):
*********啓動類加載器************ file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/resources.jar file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/rt.jar file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/sunrsasign.jar file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/jsse.jar file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/jce.jar file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/charsets.jar file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/lib/jfr.jar file:/C:/Program%20Files/Java/jdk1.8.0_151/jre/classes null
咱們經過下面代碼驗證一下:
public class ClassLoaderTest { public static void main(String[] args) { System.out.println("*********擴展類加載器************"); String extDirs = System.getProperty("java.ext.dirs"); for (String path : extDirs.split(";")) { System.out.println(path); } // Java\lib\ext目錄下隨意選擇一個類,查看他的類加載器是什麼 ClassLoader classLoader = CurveDB.class.getClassLoader(); System.out.println(classLoader); } }
獲得的結果:
*********擴展類加載器************ C:\Program Files\Java\jdk1.8.0_151\jre\lib\ext C:\WINDOWS\Sun\Java\lib\ext sun.misc.Launcher$ExtClassLoader@7ea987ac
在Java的平常應用程序開發中,類的加載幾乎是由上述3種類加載器相互配合執行的,在必要時,咱們還能夠自定義類加載器,來定製類的加載方式。
爲何要自定義類加載器?
用戶自定義類加載器實現步驟:
ClassLoader類,它是一個抽象類,其後全部的類加載器都繼承自ClassLoader(不包括啓動類加載器)。
方法名稱 | 描述 |
---|---|
getParent() | 返回該類加載器的超類加載器 |
loadClass(String 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類 |
獲取ClassLoader的途徑:
clazz.getClassLoader()
Thread.currentThread().getContextClassLoader()
ClassLoader.getSystemClassLoader()
DriverManager.getCallerClassLoader()
Java虛擬機對class文件採用的是按需加載的方式,也就是說當須要使用該類時纔會將它的class文件加載到內存生成class對象。並且加載某個類的class文件時,Java虛擬機採用的是雙親委派模式,即把優先將請求交由父類處理,它是一種任務委派模式。
下面用一個例子說明:
public class StringTest { public static void main(String[] args) { String string = new String(); System.out.println("Hello World!"); } }
而後自定義一個java.lang.String類:
public class String { static { System.out.println("這是自定義的String類的靜態代碼塊!"); } }
執行結果:Hello World!
當咱們加載jdbc.jar 用於實現數據庫鏈接的時候,首先咱們須要知道的是 jdbc.jar是基於SPI接口進行實現的,因此在加載的時候,會進行雙親委派,最終從啓動類加載器中加載 SPI核心類。而後再加載SPI接口實現類,就進行反向委派,經過線程上下文類加載器進行實現jdbc.jar
的加載。
保護程序安全,防止核心API被隨意篡改
Java安全模型的核心就是Java沙箱(sandbox)。沙箱是一個限制程序運行的環境。沙箱機制就是將 Java 代碼限定在虛擬機(JVM)特定的運行範圍中,而且嚴格限制代碼對本地系統資源訪問,經過這樣的措施來保證對代碼的有效隔離,防止對本地系統形成破壞。
組成Java沙箱的基本組件以下:
Java安全模型的前三個部分——類加載體系結構、class文件檢驗器、Java虛擬機(及語言)的安全特性一塊兒達到一個共同的目的:保持Java虛擬 機的實例和它正在運行的應用程序的內部完整性,使得它們不被下載的惡意代碼或有漏洞的代碼侵犯。相反,這個安全模型的第四個組成部分是安全管理器,它主要 用於保護虛擬機的外部資源不被虛擬機內運行的惡意或有漏洞的代碼侵犯。這個安全管理器是一個單獨的對象,在運行的Java虛擬機中,它在對於外部資源的訪 問控制起中樞做用。
例如,自定義一個java.lang.String類,可是在加載自定義String類的時候會率先使用啓動類加載器加載,而啓動類加載器在加載的過程當中會先加載jdk自帶的文件(rt.jar包java.lang.中javalangString.class),報錯信息說沒有main方法,就是由於加載的是rt.jar包中的string類。這樣能夠保證對java核心源代碼的保護,這就是沙箱安全機制。
public class String { static { System.out.println("這是自定義的String類的靜態代碼塊!"); } // 錯誤 public static void main(String[] args) { System.out.println("Hello World!"); } }
在JVM中表示兩個class對象是否爲同一個類存在兩個必要條件:
換句話說,在JVM中,即便這兩個類對象(class對象)來源同一個Class文件,被同一個虛擬機所加載,但只要加載它們的ClassLoader實例對象不一樣,那麼這兩個類對象也是不相等的。
JVM必須知道一個類型是由啓動加載器加載的仍是由用戶類加載器加載的。若是一個類型是由用戶類加載器加載的,那麼JVM會將這個類加載器的一個引用做爲類型信息的一部分保存在方法區中。當解析一個類型到另外一個類型的引用的時候,JVM須要保證這兩個類型的類加載器是相同的。
Java程序對類的使用方式分爲:主動使用和被動使用。
主動使用,又分爲七種狀況:
除了以上七種狀況,其餘使用Java類的方式都被看做是對類的被動使用,都不會致使類的初始化。