JVM之類加載子系統

上篇文章中咱們知道了JVM是個啥?這篇文章(通篇文章都是以HotSpot JVM爲例)就讓咱們來了解一下類加載子系統(ClassLoader)--負責從文件系統或者網絡中加載Class字節碼文件,並將加載的類信息(DNA元數據模版,jvm會根據這個模版實例化出n個如出一轍的實例)存放於「方法區」(接下來的文章中會作具體的介紹)中。ClassLoader只負責文件的加載,而文件是否能夠運行,則由執行引擎(Exection Engine,接下來的文章中會作具體的介紹)決定。java

下圖是類加載子系統構造圖:數據庫

類的加載過程

加載(Loading)

加載流程bootstrap

  1. 經過一個類的全限定名獲取定義此類的二進制字節流;
  2. 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構;
  3. 在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口。

加載.class文件的方式數組

  • 從本地系統中直接加載;
  • 經過網絡獲取,典型場景:Web Applet
  • 從zip壓縮包中讀取,成爲往後jarwar格式的基礎;
  • 運行時計算生成,使用最多的是:動態代理技術;
  • 由其餘文件生成,典型場景:jsp應用;
  • 從專有數據庫中提取.class文件,比較少見;
  • 從加密文件中讀取,典型的防Class文件被反編譯的保護措施。

連接(Linking)

(1)驗證(Verify)安全

  • 目的是保證Class文件的字節流中包含的信息符合當前虛擬機的要求,保證被加載類的正確性,不會危害虛擬機的自身安全。
  • 主要分爲四種驗證方式:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。

文件格式驗證:主要驗證字節流是否符合Class文件格式規範,而且能被當前的虛擬機加載處理。例如:主、次版本號是否在當前虛擬機處理的範圍以內。常量池中是否有不被支持的常量類型。指向常量的中的索引值是否存在不存在的常量或不符合類型的常量。微信

元數據驗證:對字節碼描述的信息進行語義的分析,分析是否符合java的語言語法的規範。網絡

字節碼驗證:最重要的驗證環節,分析數據流和控制,肯定語義是合法的,符合邏輯的。主要的針對元數據驗證後對方法體的驗證。保證類方法在運行時不會有危害出現。數據結構

符號引用驗證:主要是針對符號引用轉換爲直接引用的時候,是會延伸到第三解析階段,主要去肯定訪問類型等涉及到引用的狀況,主要是要保證引用必定會被訪問到,不會出現類等沒法訪問的問題。多線程

java虛擬機字節碼文件起始編碼CAFEBABE(使用Binary Viewer軟件)jvm

(2)準備(Prepare)

  • 爲類變量(靜態變量)分配內存而且設置該類變量的默認初始值,即零值。
public class HelloWord{
  //準備階段:a=0 -> 初始化階段:a=1
  private static int a = 1;
  
  public static void main(){
    System.out.println(a);
  }
}
  • 這裏不包含final修飾的static,由於final在編譯的時候就會分配了,準備階段會顯示初始化;
  • 這裏不會爲實例變量(new的對象)分配初始化,類變量會分配在方法區中,而實例變量是會隨着對象一塊兒分配到java堆中。

(3)解析(Resolve)

將常量池內的符號引用(符號引用就是一組符號來描述所引用的目標)轉換爲直接引用(直接引用就是直接指向目標的指針、相對偏移量或一個簡潔定位到目標的句柄)的過程。事實上,解析操做每每會伴隨着JVM在執行完初始化以後再執行。解析動做主要針對類或接口、字段、類方法、方法類型等。對應常量池中的CONSTANT_Class_infoCONSTANT_Fieldref_infoCONSTANT_Methodref_info等。

解析的執行過程等後邊講到字節碼文件時再作具體解釋。

初始化(Initialization)

  • 初始化階段就是執行類構造器方法 <clinit>()的過程。此方法不須要定義,是javac編譯器自動收集類中的全部類變量的賦值動做和靜態代碼塊中的語句合併而來。構造器方法中的指令按語句在源文件中出現的順序執行。<clinit>()不一樣於類的構造器,構造器是虛擬機視角下的<init>()
  • 若該類具備父類,JVM會保證子類的<clinit>()執行前,父類的<clinit>()已經執行完畢。
  • 虛擬機必須保證一個類的<clinit>()方法在多線程下被同步加鎖。

樣例:

在未定義前進行調用會致使「非法前向引用」錯誤

類的初始化時機

java程序對類的使用方式能夠分爲兩種:

1. 主動使用

  • 建立類的實例
  • 訪問某個類或接口的靜態變量,或者對該靜態變量賦值
  • 調用類的靜態方法
  • 反射
  • 初始化一個類的子類
  • Java虛擬機啓動被標明爲啓動類的類
  • JDK 7 開始提供的動態語言支持:java.lang.invoke.MethodHandle實例的解析結果,REF_getStaticREF_putStaticREF_invokeStatic句柄對應的類沒有初始化,則初始化。

2. 被動使用:除了以上七種狀況,其餘都被看做是類的被動使用,都不會致使類的初始化。

類的加載器

加載器分類

JVM支持兩種類型的類加載器,分別爲引導類加載器(Bootstrap ClassLoader)和自定義加載器(User-Defined ClassLoader),他們之間不是繼承關係,而是包含關係。

引導類加載器

引導類加載器又稱爲啓動類加載器,該類是使用C/C++語言實現的,嵌套在JVM內部。它用來加載Java的核心類庫(JAVA_HOME/jre/lib/rt.jarresources.jarsun.boot.class.path路徑下的內容),用於提供jvm自身須要的類,出於安全考慮,Bootstrap啓動類只加載包名爲javajavaxsun等開頭的類。它並不繼承自java.lang.ClassLoader,沒有父加載器。

//String屬於java的核心類庫--->使用引導類加載器進行加載
ClassLoader classLoader1 = String.class.getClassLoader();
System.out.println(classLoader1);//null

自定義加載器

自定義加載器是指全部派生於抽象類CLassLoader的類加載器,它分爲擴展類加載器、應用程序(系統)加載器、用戶自定義加載器。

(1)擴展類加載器

java語言編寫,由sun.misc.Launcher.ExtClassLoader實現。其父類加載器爲啓動類加載器,從java.ext.dirs系統屬性所指定的目錄中加載類庫,或從jdk的安裝目錄的jre/lib/ext子目錄(擴展目錄)下加載類庫。若是用戶建立的jar放在此目錄下,也會自動由擴展類加載器加載。

(2)系統加載器

java語言編寫,由sun.misc.Launcher.AppClassLoader實現。父類加載器爲擴展類加載器,負責加載環境變量classpath或系統屬性,java.class.path指定路徑下的類庫。該類加載是程序中默認的類加載器。

//獲取系統類加載器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

//獲取其上層:擴展類加載器
ClassLoader extClassLoader = systemClassLoader.getParent();
System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@4554617c

//獲取引導類加載器
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
System.out.println(bootstrapClassLoader);//null

(3)用戶自定義加載器

開發人員能夠經過繼承抽象類java.lang.ClassLoader,並實現findClass()方法來實現自定義類加載器。在編寫自定義類加載器時,若是沒有太過於複雜的需求,能夠直接繼承URLClassLoader類,這樣就能夠避免本身去編寫findClass()方法及其獲取字節碼流的方式,使自定義類加載器編寫更加簡潔。

//用戶自定義類:默認使用系統類加載器進行加載
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

爲何要自定義加載器?

  • 隔離加載類;
  • 修改類加載方式;
  • 擴展加載源;
  • 防止源碼泄漏;

ClassLoader的獲取與API

獲取

  1. 獲取當前類的ClassLoaderclazz.getClassLoader();
  2. 獲取當前線程上下文的ClassLoaderThread.currentThread().getContextClassLoader();
  3. 獲取系統的ClassLoaderClassLoader.getSystemClassLoader();
  4. 獲取調用者的ClassLoaderDriverManager.getCallerClassLoader();

API

  • Class loadClass(String name) :name參數指定類裝載器須要裝載類的名字,必須使用全限定類名,如:com.smart.bean.Car。該方法有一個重載方法 loadClass(String name,boolean resolve)``,resolve`參數告訴類裝載時候須要解析該類,在初始化以前,因考慮進行類解析的工做,但並非全部的類都須要解析。若是JVM只須要知道該類是否存在或找出該類的超類,那麼就不須要進行解析。
  • Class defineClass(String name,byte[] b,int len):將類文件的字節數組轉換成JVM內部的java.lang.Class對象。字節數組能夠從本地文件系統、遠程網絡獲取。參數name爲字節數組對應的全限定類名。
  • Class findSystemClass(String name):從本地文件系統加載Class文件。若是本地系統不存在該Class文件。則拋出ClassNotFoundException異常。該方法是JVM默認使用的裝載機制
  • Class findLoadedClass(String name):調用該方法來查看ClassLoader是否已載入某個類。若是已載入,那麼返回java.lang.Class對象;不然返回null。若是強行裝載某個已存在的類,那麼則拋出連接錯誤。
  • ClassLoader getParent():獲取類裝載器的父裝載器。除根裝載器外,全部的類裝載器都有且僅有一個父裝載器。ExtClassLoader的父裝載器是根裝載器,由於根裝載器非java語言編寫,因此沒法獲取,將返回null

雙親委派機制

Java虛擬機對class文件採用的是按需加載的方式,也就是說當須要使用該類時纔會將它的class文件加載到內存生成class對象。並且加載某個類的class文件時,Java虛擬機採用的是雙親委派模式,即把請求交由父類處理,它是一種任務委派模式。

工做原理

若是一個類加載器收到了類加載請求,它並不會本身先去加載,而是把這個請求委託給父類的加載器去執行。若是父類加載器還存在其父類加載器,則進一步向上委託,依次遞歸,請求最終將到達頂層的啓動類加載器。若是父類加載器能夠完成類加載任務,就成功返回,假若父類加載器沒法完成此加載任務,子加載器纔會嘗試本身去加載,這就是雙親委派模式。

優點

  • 避免類的重複加載,當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次。
  • 保護程序安全,防止核心API被隨意篡改。舉例代碼截圖以下

如圖所示,咱們建立java.lang.String類,當在加載自定義類的時候會先使用引導類加載器加載,而引導類加載器在加載的過程當中會先加載jdk自帶的文件(rt.jar包中的java/lang/String.class)。報錯信息說沒有main方法,就是由於加載的是rt.jar包下的String類。這樣咱們就能保證對java的核心源代碼進行保護,這就是沙箱安全機制。由此可知JVM中判斷兩個Class對象是不是同一個類存在兩個必要條件:一是類的完整類名必須保持一致,包括包名;二是加載該類的類加載器必須相同。

對類加載器的引用

JVM必須知道一個類是由啓動類加載器仍是用戶類加載器加載的,若是一個類是由用戶類加載器加載的,那麼jvm會將這個類加載器的一個引用做爲類信息的一部分保存到方法區中,當解析一個類到另外一個類的引用的時候,jvm須要保證兩個類的類加載器是相同的。

加載器部分的內容就說到這裏了,若是你感興趣的話,能夠關注微信公衆號「阿Q說代碼」!你也能夠加阿Q好友qingqing-4132,期待你的到來!

相關文章
相關標籤/搜索