Java筆記(class loader)

摘自:https://www.ibm.com/developerworks/cn/java/j-lo-classloader/java

        《與深刻理解Java虛擬機》 sql

1.類加載器基本概念
顧名思義,類加載器(class loader)用來加載 Java 類到 Java 虛擬機中。通常來講,Java 虛擬機使用 Java 類的方式以下:Java 源程序(.java 文件)在通過 Java 編譯器編譯以後就被轉換成 Java 字節代碼(.class 文件)。類加載器負責讀取 Java 字節代碼,並轉換成java.lang.Class類的一個實例。每一個這樣的實例用來表示一個 Java 類。經過此實例的 newInstance()方法就能夠建立出該類的一個對象。實際的狀況可能更加複雜,好比 Java 字節代碼多是經過工具動態生成的,也多是經過網絡下載的。

2.類加載的流程
類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括了:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(using)、和卸載(Unloading)七個階段。其中驗證、準備和解析三個部分統稱爲鏈接(Linking),這七個階段的發生順序以下圖所示:

類的生命週期

 

如上圖所示,加載、驗證、準備、初始化和卸載這五個階段的順序是肯定的,類的加載過程必須按照這個順序來循序漸進地開始,而解析階段則不必定,它在某些狀況下能夠在初始化階段後再開始。 數據庫

類的生命週期的每個階段一般都是互相交叉混合式進行的,一般會在一個階段執行的過程當中調用或激活另一個階段。 編程

    Java虛擬機規範沒有強制性約束在何時開始類加載過程,可是對於初始化階段,虛擬機規範則嚴格規定了有且只有5種狀況必需當即對類進行「初始化」(而加載、驗證、準備階段則必需在此以前開始),這五種狀況歸類以下: 數組

  • 遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,若是類沒有進行過初始化,則須要先觸發其初始化。生成這4條指令最多見的Java代碼場景是:使用new關鍵字實例化對象時、讀取或者設置一個類的靜態字段(被final修飾、已在編譯器把結果放入常量池的靜態字段除外)時、以及調用一個類的靜態方法的時候。 緩存

  • 使用java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則須要先觸發其初始化。 安全

  • 當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要觸發父類的初始化。 網絡

  • 當虛擬機啓動時,用戶須要指定一個執行的主類(包含main()方法的類),虛擬機會先初始化這個類。 數據結構

  • 當使用JDK1.7的動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後解析結果REF_getStatic、REF_putStatic、REF_invokeStatic、的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化。 多線程

對象的建立:

有4種顯式地建立對象的方式:

  1. 用new語句建立對象,這是最經常使用的建立對象的方式。

  2. 運用反射手段,調用java.lang.Class或者java.lang.reflect.Constructor類的newInstance()實例方法。

  3. 調用對象的clone()方法。

  4. 運用反序列化手段,調用java.io.ObjectInputStream對象的readObject()方法.

除了以上4種顯式地建立對象的方式之外,在程序中還能夠隱含地建立對象,包括如下幾種狀況:

  1. 對於java命令中的每一個命令行參數,Java虛擬機都會建立相應的String對象,並把它們組織到一個String數組中,再把該數組做爲參數傳給程序入口main(String args[])方法。
  2. 程序代碼中的String類型的直接數對應一個String對象,例如:
    String s1="Hello";
    
    String s2="Hello"; //s2和s1引用同一個String對象
    
    String s3=new String("Hello");
    
    System.out.println(s1==s2); //打印true
    
    System.out.println(s1==s3); //打印false
    執行完以上程序,內存中實際上只有兩個String對象,一個是直接數,由Java虛擬機隱含地建立,還有一個經過new語句顯式地建立。
  3. 字符串操做符「+」的運算結果爲一個新的String對象。例如:
    String s1="H";
    
    String s2=" ello";
    
    String s3=s1+s2; //s3引用一個新的String對象
    
    System.out.println(s3=="Hello"); //打印false
    
    System.out.println(s3.equals("Hello")); //打印true
  4. 當Java虛擬機加載一個類時,會隱含地建立描述這個類的Class實例.

不可變類String類型在建立的時候會在虛擬機中建立一個String對象。

可變類:當你得到這個類的一個實例引用時,你能夠改變這個實例的內容。
不可變類:當你得到這個類的一個實例引用時,你不能夠改變這個實例的內容。不可變類的實例一但建立,其內在成員變量的值就不能被修改。


爲何String類是不可變的?

String是全部語言中最經常使用的一個類。咱們知道在Java中,String是不可變的、final的。Java在運行時也保存了一個字符串池(String pool),這使得String成爲了一個特別的類。

String類不可變性的好處

  1. 只有當字符串是不可變的,字符串池纔有可能實現。字符串池的實現能夠在運行時節約不少heap空間,由於不一樣的字符串變量都指向池中的同一個字符串。但若是字符串是可變的,那麼String interning將不能實現(譯者注:String interning是指對不一樣的字符串僅僅只保存一個,即不會保存多個相同的字符串。),由於這樣的話,若是變量改變了它的值,那麼其它指向這個值的變量的值也會一塊兒改變。

  2. 若是字符串是可變的,那麼會引發很嚴重的安全問題。譬如,數據庫的用戶名、密碼都是以字符串的形式傳入來得到數據庫的鏈接,或者在socket編程中,主機名和端口都是以字符串的形式傳入。由於字符串是不可變的,因此它的值是不可改變的,不然黑客們能夠鑽到空子,改變字符串指向的對象的值,形成安全漏洞。

  3. 由於字符串是不可變的,因此是多線程安全的,同一個字符串實例能夠被多個線程共享。這樣便不用由於線程安全問題而使用同步。字符串本身即是線程安全的。

  4. 類加載器要用到字符串,不可變性提供了安全性,以便正確的類被加載。譬如你想加載java.sql.Connection類,而這個值被改爲了myhacked.Connection,那麼會對你的數據庫形成不可知的破壞。

  5. 由於字符串是不可變的,因此在它建立的時候hashcode就被緩存了,不須要從新計算。這就使得字符串很適合做爲Map中的鍵,字符串的處理速度要快過其它的鍵對象。這就是HashMap中的鍵每每都使用字符串。

原文連接: Journaldev 翻譯: ImportNew.com - 唐小娟


 

3.類加載的過程

1.加載

在加載階段,虛擬機完成如下3件事:

  1. 經過一個類的全限定名來獲取定義此類的二進制字節流,如能夠從ZIP包中讀取(JAR,EAR,WAR格式的基礎),從網絡中獲取(Applet),運行時計算生成(動態代理技術,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass來爲特定接口生成形式爲"*$Proxy"的代理類的二進制字節流),由其餘文件生成(JSP文件生成對應的Class類),從數據庫中讀取。。。

  2. 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構

  3. 在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口。

加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,方法區中的數據存儲格式由虛擬機實現自行定義,虛擬機規範未規定此區域的具體數據結構。而後再內存中實例化一個java.lang.Class類的對象,這個對象將做爲程序訪問方法區中的這些類型數據的外部接口。

加載數據與連接階段的部份內容(若是一部分字節碼文件格式驗證動做)是交叉進行的,加載階段還沒有完成完成,連接階段可能已經開始,但這些夾在夾在夾在階段致之中的動做,仍然屬於鏈接段的內容,這兩個階段的開始時間仍然保持着固定的前後順序

2.驗證


     驗證時連接階段的第一步,這一階段的目的是微利確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。

     驗證階段大體上會完成一下4個階段的檢驗動做:文件格式的驗證、元數據的驗證、字節流驗證、符號引用的驗證

文件格式的驗證

     第一階段要驗證字節流是否符合Class文件格式的規範,而且能被當前版本的虛擬機處理。可能包括如下驗證點:

    1. 是否以魔方數0xCAFEBABE開頭

    2. 主、次版本號是否在當前虛擬機的處理範圍以內

    3. 常量池的常量中是否有不被支持的常量類型(檢驗常量tag標誌)

    4. 指向常量的各類索引值中是否有指向不存在的常量或不符合類型的常量

    5. CONSTANT_Utf8_info型的常量中是否有不符合UTF8編碼的數據

    6. Class文件中各個部分及文件自己

 

  • 。。。。。。

 

該驗證階段的主要目的是保證輸入的字節流能正確的解析並存儲於方法區以內,格式上符合一個Java類型信息的要求。這階段的驗證時基於二進制字節流進行的,只有經過了這個階段的驗證後,字節流纔會進入內存的方法區進行存儲,因此後面的3個驗證階段所有是基於方法區的存儲結構進行的,不會再直接操做字節流。

元數據驗證

 

第二階段是對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求,這個階段可能包括的驗證點以下:

    1. 這個類是否有父類(除了java.lang.Object以外,全部的類都應當有父類)

 

  • 這個類的父類是否繼承了不容許被繼承的父類

  • 若是這個類不是抽象類,是否實現了其父類或接口之中要求實現的全部方法

  • 類中的字段、方法是否與父類產生矛盾(例如覆蓋了父類的final字段,或者出現不符合規則的方法重載,例如方法參數都不一致,但返回值類型卻不一樣等)

 

字節碼驗證

第三階段是整個驗證過程當中最複雜的一個階段,主要目的是經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。在第二階段對元數據信息中的數據類型作完校驗後,這個階段將對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會作出危害虛擬機安全的事件,例如:

    1. 保證任意時刻操做數棧的數據類型與指令代碼序列都能配合工做,例如不會出現相似這樣的狀況:在操做棧放置了一個int類型的數據,使用時卻按long類型來加載如本地變量表中

 

  • 保證跳轉指令不會跳轉到方法體之外的字節碼指令上

  • 保證跳轉指令不會跳轉到方法體之外的字節碼指令上

  • 保證方法體中的類型轉換是有效的,例如能夠把一個子類對象賦給父類數據類型,這是安全的,可是把父類對象賦給子類數據類型,甚至是把對象賦給與它毫無繼承關係、徹底不相干的一個數據類型,則是危險和不合法的

 

符號引用引證

主要是在虛擬機將符號引用轉化爲直接引用的時候進行校驗,這個轉化動做是發生在解析階段。符號引用驗證能夠看作是對類自身之外(常量池的各類符號引用)的信息進行匹配性的校驗。一般須要校驗下列內容:

符號引用中經過字符串描述的全限定名是否經過字符串描述的全限定名是否能找到對應的類

在指定類中是否在符合方法的字段描述符以及簡單名稱所描述的方法和字段

符號引用中的類、字段、方法的訪問性(private、protected、public、default)是否可被當前類訪問

驗證階段對於虛擬機的類加載機制來講,是一個很是重要但不必定是必要的階段。若是所運行的所有代碼都已經被反覆使用和驗證過,在實施階段就能夠考慮使用-Xverify:none參數來關閉大部分的類驗證措施,從而縮短虛擬機類加載的時間。

3.準備

準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中進行分配。注:這個時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒被分配在Java堆中。

4.解析


   解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。

    符號引用(Symbolic Reference):符號引用以一組符號來描述所引用的目標,符號引用能夠是任何形式的字面量,符號引用與虛擬機實現的內存佈局無關,引用的目標並不必定已經在內存中。

    直接引用(Direct Reference):直接引用能夠是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現的內存佈局相關的,同一個符號引用在不一樣的虛擬機實例上翻譯出來的直接引用通常都不相同,若是有了直接引用,那引用的目標一定已經在內存中存在。

    對於同一個符號引用可能會出現屢次解析,虛擬機可能會對第一次解析的結果進行緩存。

    解析動做分爲四類:包括類或接口的解析、字段解析、類方法解析、接口方法解析。

5.初始化

 

類初始化階段是類加載過程的最後一步,前面的類加載過程當中,除了加載(Loading)階段用戶應用程序能夠經過自定義類加載器參與以外,其他動做徹底由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的Java程序代碼。

    初始化階段是執行類構造器<clinit>()方法的過程。對於<clinit>()方法具體介紹以下:

    1)<clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊(static{}塊)中的語句合併產生的,編譯器收集的順序由語句在源文件中出現的順序所決定。

    2)<clinit>()方法與類的構造函數不一樣,它不須要顯式地調用父類構造器,虛擬機會保證在子類的<clinit>()方法執行以前,父類的<clinit>()方法已經執行完畢,所以在虛擬機中第一個執行的<clinit>()方法的類必定是java.lang.Object。

    3)因爲父類的<clinit>()方法先執行,也就意味着父類中定義的靜態語句塊要優先於子類的變量賦值操做。

    4)<clinit>()方法對於類或者接口來講並非必需的,若是一個類中沒有靜態語句塊也沒有對變量的賦值操做,那麼編譯器能夠不爲這個類生成<clinit>()方法。

    5)接口中可能會有變量賦值操做,所以接口也會生成<clinit>()方法。可是接口與類不一樣,執行接口的<clinit>()方法不須要先執行父接口的<clinit>()方法。只有當父接口中定義的變量被使用時,父接口才會被初始化。另外,接口的實現類在初始化時也不會執行接口的<clinit>()方法。

    6)虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖和同步。若是有多個線程去同時初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其它線程都須要阻塞等待,直到活動線程執行<clinit>()方法完畢。若是在一個類的<clinit>()方法中有耗時很長的操做,那麼就可能形成多個進程阻塞。

4.類與類加載器

對於任意一個類,都須要由加載它的類加載器和這個類自己一同確立其在Java虛擬機中的惟一性,對於每個類加載器,都擁有一個獨立的類名稱空間。通俗講:比較兩個類是否「相等」,只有在這兩個類由一個類加載器加載的前提下才有意義,不然,即便這兩個類來源於同一個Class文件,被同一個虛擬機加載,只要加載它們的類加載器不一樣,那這兩個類就一定不一樣。

這裏的「相等」,包括表明類的Class對象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回結果,也包括使用instanceof關鍵字作對象所屬關係判斷等狀況

雖然來自同一個Class文件,若是被兩個不一樣的類加載器加載,但依然是兩個獨立的類,作對象所屬類型檢驗時結果爲false

5.雙親委派模型

從Java虛擬機的角度來說,只存在兩種不一樣的類加載器:一種是啓動類加載器(Bootstarp ClassLoader),這個類加載器使用C++語言實現,是虛擬機自身的一部分;另外一個就是全部其餘的類加載器,這些類加載器都由Java語言實現,獨立於虛擬機外部,而且全都繼承自抽象類java.lang.ClassLoader

從Java開發人員的角度來看,類加載器還能夠爲:

  • 啓動類加載器(Bootstarp ClassLoader):這個類將負責將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath參數所指定的路徑中,而且是虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫即便放在lib目錄中也不會被加載)類庫加載到虛擬機內存中。啓動類加載器沒法被Java程序直接引用,用戶在編寫自定義類加載器時,若是須要把加載器請求委派給引導類加載器,那直接使用null代替便可。
  • 擴展類加載器(Extension ClassLoader):這個加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑中的全部類庫,開發者能夠直接使用擴展類加載器
  • 應用程序類加載器(Application ClassLoder):這個類加載由sun.misc.Launcher$AppClassLoader實現。因爲這個類加載器是ClassLoader中的getSystemClassLoader()方法的返回值,也稱爲系統類加載器,它負責加載用戶路徑(ClassPath)上指定的類庫。通常來講,Java 應用的類都是由它來完成加載的。能夠經過 ClassLoader.getSystemClassLoader()來獲取它。

類加載器之間這種層次關係,稱爲類加載器的雙親委派模型(Parents Delegation Model)

類加載器的雙親委派模型

 

 

 

 

 

 

 

 

 

 

 

 

 

 

雙親委派模型的工做過程:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每個層次的類加載器都是如此,所以全部的加載請求最終都應該傳送到父類加載器去完成,每個層次的類加載器反饋本身沒法完成這個請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試本身去加載。

雙親委派模型對於保證Java程序的穩定運做很重要,但它的實現卻很是簡單,實現雙親委派的代碼都集中在java.lang.ClassLoader()方法中。

6.java.lang.ClassLoader類介紹

java.lang.ClassLoader類的基本職責就是根據一個指定的類的名稱,找到或者生成其對應的字節代碼,而後從這些字節代碼中定義出一個 Java 類,即 java.lang.Class類的一個實例。除此以外,ClassLoader還負責加載 Java 應用所需的資源,如圖像文件和配置文件等。不過本文只討論其加載類的功能。爲了完成加載類的這個職責,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類的實例。這個方法被聲明爲 final的。

resolveClass(Class<?> c) 連接指定的 Java 類。

相關文章
相關標籤/搜索