以下圖所示(圖片來源於網路),JVM類加載機制分爲五個部分:加載,驗證,準備,解析,初始化,下面咱們就分別來看一下這五個過程。html
若是一個類或者接口 C 不是數組類,那麼 C 使用 類加載器 加載二進制表示,因爲數組類不具備外部二進制表示形式,它們由 Java 虛擬機建立,而不是由類加載器建立。java
加載是類加載過程當中的一個階段,這個階段會在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的入口。注意這裏不必定非得要從一個Class文件獲取,這裏既能夠從ZIP包中讀取(好比從jar包和war包中讀取),也能夠在運行時計算生成(動態代理),也能夠由其它文件生成(好比將JSP文件轉換成對應的Class類)。數據庫
所作的3件事:數組
2.1安全
驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。
驗證階段大體會完成4個階段的檢驗動做:網絡
驗證階段是很是重要的,但不是必須的,它對程序運行期沒有影響,若是所引用的類通過反覆驗證,那麼能夠考慮採用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間數據結構
準備階段是正式爲類變量分配內存並設置類變量的初始值階段,即在方法區中分配這些變量所使用的內存空間。注意這裏所說的初始值概念,好比一個類變量定義爲:函數
public static int v = 8080;
實際上變量v在準備階段事後的初始值爲0而不是8080,將v賦值爲8080的putstatic指令是程序被編譯後,存放於類構造器<client>方法之中,這裏咱們後面會解釋。
可是注意若是聲明爲:佈局
public static final int v = 8080;
在編譯階段會爲v生成ConstantValue屬性,在準備階段虛擬機會根據ConstantValue屬性將v賦值爲8080。this
解析階段是指虛擬機將常量池中的符號引用替換爲直接引用的過程。符號引用就是class文件中的:
等類型的常量。
下面咱們解釋一下符號引用和直接引用的概念:
初始化階段是類加載最後一個階段,前面的類加載階段以後,除了在加載階段能夠自定義類加載器之外,其它操做都由JVM主導。到了初始階段,纔開始真正執行類中定義的Java程序代碼。
初始化階段是執行類構造器<clinit>方法的過程。<clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊static{}中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句塊能夠賦值,可是不能訪問。
p.s: 若是一個類中沒有對靜態變量賦值也沒有靜態語句塊,那麼編譯器能夠不爲這個類生成<clinit>()方法。
<clinit>()方法與實例構造器<init>()方法不一樣,它不須要顯示地調用父類構造器,虛擬機會保證在子類<clinit>()方法執行以前,父類的<clinit>()方法已經執行完畢.
注意如下幾種狀況不會執行類初始化:
虛擬機設計團隊把加載動做放到JVM外部實現,以便讓應用程序決定如何獲取所需的類,JVM提供了3種類加載器:
JVM經過 雙親委派模型 和 沙箱機制 進行類的加載,固然咱們也能夠經過繼承java.lang.ClassLoader實現自定義的類加載器。
當一個類加載器收到類加載任務,會先交給其父類加載器去完成,所以最終加載任務都會傳遞到頂層的啓動類加載器,只有當父類加載器沒法完成加載任務時,纔會嘗試執行加載任務。
採用雙親委派的一個好處是好比加載位於rt.jar包中的類java.lang.Object,不論是哪一個加載器加載這個類,最終都是委託給頂層的啓動類加載器進行加載,這樣就保證了使用不一樣的類加載器最終獲得的都是一樣一個Object對象。
在有些情境中可能會出現要咱們本身來實現一個類加載器的需求。咱們直接看一下jdk8中的ClassLoader的源碼實現:
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) { long t0 = System.nanoTime(); 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. long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
而上面的findClass()的實現以下,直接拋出一個異常,而且方法是protected,很明顯這是留給咱們開發者本身去實現的。
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
執行下面的代碼後,輸出什麼呢?
package com.ycit; /** * @author chenxiaolei * @date 2019/3/26 */ public class StaticTest { public static void main(String[] args) { staticFunction(); } static StaticTest st = new StaticTest(); static { System.out.println("1"); } { System.out.println("2"); } StaticTest() { System.out.println("3"); System.out.println("a="+a+",b="+b); } public static void staticFunction(){ System.out.println("4"); } int a=110; static int b =112; }
正確答案:
2 3 a=110,b=0 1 4
解析:
按照上面對 類加載過程的解讀,
準備階段,爲類變量賦值,這裏是賦的是默認值,即 st = null,b=0;
初始化階段,是執行類構造器<client>方法的過程。。<clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊static{}中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的
先 new StaticTest(),對象的初始化是先初始化成員變量再執行構造方法,即,a = 100;> 而後 打印 2,> 而後構造函數中的 打印 3, a=100,b=0;>接着執行static 代碼塊,打印1;> 而後 b 賦值 12;最後執行函數,打印4;
通常按照下面的套用規則順序,可是上面的 靜態變量 是對象初始化,結果就不同了。