Java類加載整理總結

1.概述

一個Java文件從編寫到最後地執行其實總結起來就是兩步:1.編譯;2.運行。編譯,即把我們寫好的java文件,通過javac命令編譯成字節碼,也就是我們常說的.class文件。運行,則是把編譯生成的.class文件交給Java虛擬機(JVM)執行。而虛擬機把描述類的數據從Class文件加載到內存中,並對數據進行驗證,準備,解析,初始化,最終生成虛擬機可以直接使用的java類型,這個過程就叫做類加載。

2.加載機制

類加載過程如下圖所示:

1.加載  

               1.1 加載流程 

在加載階段,虛擬機需要完成以下三件事情:

    1、通過一個類的全限定名來獲取其定義的二進制字節流。
    2、將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構。
    3、在Java堆中生成一個代表這個類的java.lang.Class對象,作爲對方法區中這些數據的訪問入口。


        注意:這裏第1條中的二進制字節流並不只是單純地從Class文件中獲取,比如它還可以從Jar包中獲取、從網絡中獲取(最典型的應用便是Applet)、由其他文件生成(JSP應用)等。
        相對於類加載的其他階段而言,加載階段(準確地說,是加載階段獲取類的二進制字節流的動作)是可控性最強的階段,因爲開發人員既可以使用系統提供的類加載器來完成加載,也可以自定義自己的類加載器來完成加載。
    加載階段完成後,虛擬機外部的 二進制字節流就按照虛擬機所需的格式存儲在方法區之中,而且在Java堆中也創建一個java.lang.Class類的對象,這樣便可以通過該對象訪問方法區中的這些數據。


        1.2 類加載器介紹

   1.2.1 類加載器可以大致劃分爲以下三類:

  • 啓動類加載器:負責加載$JAVA_HOME中jre/lib/裏所有的 class(JDK 代表 JDK 的安裝目錄,下同),或被-Xbootclasspath參數指定的路徑中的,並且能被虛擬機識別的類庫(如 rt.jar,所有的java.*開頭的類均被 Bootstrap ClassLoader 加載)。啓動類加載器由 C++ 實現,不是 ClassLoader 子類。無法被 Java 程序直接引用的。
  • 擴展類加載器:該加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載JDK\jre\lib\ext目錄中,或者由java.ext.dirs系統變量指定的路徑中的所有類庫(如javax.*開頭的類),開發者可以直接使用擴展類加載器。
  • 應用程序類加載器:該類加載器由sun.misc.Launcher$AppClassLoader來實現,它負責加載用戶類路徑(ClassPath)所指定的類,開發者可以直接使用該類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

  1.2.2 類加載器總結:

  • 啓動類加載器:Bootstrap ClassLoader 它使用 C++ 實現(這裏僅限於 Hotspot,也就是 JDK1.5 之後默認的虛擬機,有很多其他的虛擬機是用 Java 語言實現的),是虛擬機自身的一部分。 
  • 所有其他的類加載器:Extension ClassLoader、Application ClassLoader、自定義類加載器等這些類加載器都由 Java 語言實現,獨立於虛擬機之外,並且全部繼承自抽象類 java.lang.ClassLoader,這些類加載器需要由啓動類加載器加載到內存中之後才能去加載其他的類。 

       應用程序都是由這三種類加載器互相配合進行加載的,如果有必要,我們還可以加入自定義的類加載器。因爲JVM自帶的ClassLoader只是懂得從本地文件系統加載標準的java class文件,因此如果編寫了自己的ClassLoader,便可以做到如下幾點:
 1)在執行非置信代碼之前,自動驗證數字簽名。
 2)動態地創建符合用戶特定需要的定製化構建類。
 3)從特定的場所取得java class,例如數據庫中和網絡中。
事實上當使用Applet的時候,就用到了特定的ClassLoader,因爲這時需要從網絡上加載java class,並且要檢查相關的安全信息,應用服務器也大都使用了自定義的ClassLoader技術。

1.3 類加載器的加載機制

    1.3.1雙親委派模型

     java一個類的加載一般是按照雙親委派機制來走:自下而上尋找校驗加載的一個過程。大致模型如下圖所示 :

雙親委派模型的工作過程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試自己去加載。 

使用這種方式的好處:能夠有效確保一個類的全局唯一性,當程序中出現多個全限定名相同的類時,類加載器在執行加載時,始終只會加載其中的某一個類。(可以防止一些惡意的第三方類的注入替換基礎類)

使用雙親委派模型來組織類加載器之間的關係,有一個顯而易見的好處就是Java類隨着它的類加載器一起具備了一種帶有優先級的層次關係。例如類java.lang.Object,它存放在rt.jar之中,無論哪一個類加載器要加載這個類,最終都是委派給處於模型最頂端的啓動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶自己編寫了一個稱爲java.lang.Object的類,並放在程序的Class-Path中,那系統中將會出現多個不同的Object類,Java類型體系中最基礎的行爲也就無法保證,應用程序也將會變得一片混亂。如果自己去編寫一個與rt.jar類庫中已有類重名的Java類,將會發現可以正常編譯,但永遠無法被加載運行。
          雙親委派模型對於保證Java程序的穩定運作很重要,但它的實現卻非常簡單,實現雙親委派的代碼都集中在java.lang.ClassLoader的loadClass()方法之中,邏輯清晰易懂:先檢查是否已經被加載過,若沒有加載則調用父加載器的loadClass()方法,若父加載器爲空則默認使用啓動類加載器作爲父加載器。如果父類加載失敗,拋出ClassNotFoundException異常後,再調用自己的findClass()方法進行加載。
         雙親委派機制只是Java虛擬機規範建議採用的加載機制,實際在tomcat中,類加載器所採用的加載機制與傳統的雙親委派模型有一定的區別,當缺省的類加載器接收到一個類的加載任務時,首先會去由它自行加載,當它加載失敗時,纔會將類的加載任務委派給它的超類加載器去執行。

1.4 自定義類加載器

程序中如果沒有顯式指定類加載器的話,默認是AppClassLoader來加載,它負責加載ClassPath目錄中的所有類型,如果被加載的類型並沒有在ClassPath目錄中時,拋出java.lang.ClassNotFoundException異常。
           一般是繼承ClassLoader,如果要符合雙親委派規範,則重寫findClass方法;要破壞的話,重寫loadClass方法。
雙親委派模型的第一次“被破壞”其實發生在雙親委派模型出現之前——即JDK 1.2發佈之前。由於雙親委派模型在JDK 1.2之後才被引入,而類加載器和抽象類java.lang.ClassLoader則在JDK1.0時代就已經存在,面對已經存在的用戶自定義類加載器的實現代碼,Java設計者引入雙親委派模型時不得不做出一些妥協。爲了向前兼容,JDK 1.2之後的java.lang.ClassLoader添加了一個新的protected方法findClass(),在此之前,用戶去繼承java.lang.ClassLoader的唯一目的就是爲了重寫loadClass()方法,因爲虛擬機在進行類加載的時候會調用加載器的私有方法loadClassInternal(),而這個方法的唯一邏輯就是去調用自己的load-Class()。
雙親委派的具體邏輯就實現在loadClass()方法之中,JDK1.2之後已不提倡用戶再去覆蓋loadClass()方法,而應當把自己的類加載邏輯寫到findClass()方法中,在loadClass()方法的邏輯裏如果父類加載失敗,則會調用自己的findClass()方法來完成加載,這樣就可以保證新寫出來的類加載器是符合雙親委派規則的。

2.驗證

這一階段的主要目的是爲了確保Class文件的字節流中包含的信息是否符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。
主要包括驗證內容涵蓋了類數據信息的格式驗證、語義分析、操作驗證:

  • 格式驗證:驗證是否符合class文件規範
  • 語義驗證:檢查一個被標記爲final的類型是否包含子類;檢查一個類中的final方法是否被子類進行重寫;確保父類和子類之間沒有不兼容的一些方法聲明(比如方法簽名相同,但方法的返回值不同)
  • 操作驗證:在操作數棧中的數據必須進行正確的操作,對常量池中的各種符號引用執行驗證(通常在解析階段執行,檢查是否通過符號引用中描述的全限定名定位到指定類型上,以及類成員信息的訪問修飾符是否允許訪問等)

注意:

驗證階段是非常重要的,但不是必須的,它對程序運行期沒有影響,如果所引用的類經過反覆驗證,那麼可以考慮採用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。

3.準備

準備階段是正式爲類變量分配內存並設置類變量的初始值階段,即在方法區中分配這些變量所使用的內存空間。注意這裏所說的初始值概念,比如一個類變量定義爲:

public static int v = 8080;

          實際上變量v在準備階段過後的初始值爲0而不是8080,將v賦值爲8080的putstatic指令是程序被編譯後,存放於類構造器<client>方法之中。
但是注意如果聲明爲:

在編譯階段會爲v生成ConstantValue屬性,在準備階段虛擬機會根據ConstantValue屬性將v賦值爲8080。原因:可以參考一下ConstantValue屬性。

public static final int v = 8080;


     4.解析

將常量池中所有的符號引用轉爲直接引用(得到類或者字段、方法在內存中的指針或者偏移量,以便直接調用該方法)。這個階段可以在初始化之後再執行。(在編譯階段會爲v生成ConstantValue屬性,在準備階段虛擬機會根據ConstantValue屬性將v賦值爲8080)
符號引用就是class文件中的:
CONSTANT_Class_info
CONSTANT_Field_info
CONSTANT_Method_info
等類型的常量。
下面我們解釋一下符號引用直接引用的概念:

符號引用與虛擬機實現的佈局無關,引用的目標並不一定要已經加載到內存中。各種虛擬機實現的內存佈局可以各不相同,但是它們能接受的符號引用必須是一致的,因爲符號引用的字面量形式明確定義在Java虛擬機規範的Class文件格式中。

直接引用可以是指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄。如果有了直接引用,那引用的目標必定已經在內存中存在。

      5.初始化

將一個類中所有被static關鍵字標識的代碼統一執行一遍,如果執行的是靜態變量,那麼就會使用用戶指定的值覆蓋之前在準備階段設置的初始值;如果執行的是static代碼塊,那麼在初始化階段,JVM就會執行static代碼塊中定義的所有操作。
所有類變量初始化語句和靜態代碼塊都會在編譯時被前端編譯器放在收集器裏頭,存放到一個特殊的方法中,這個方法就是<clinit>方法,即類/接口初始化方法。該方法的作用就是初始化一箇中的變量,使用用戶指定的值覆蓋之前在準備階段裏設定的初始值。任何invoke之類的字節碼都無法調用<clinit>方法,因爲該方法只能在類加載的過程中由JVM調用。
如果父類還沒有被初始化,那麼優先對父類初始化,但在<clinit>方法內部不會顯示調用父類的<clinit>方法,由JVM負責保證一個類的<clinit>方法執行之前,它的父類<clinit>方法已經被執行。
          JVM必須確保一個類在初始化的過程中,如果是多線程需要同時初始化它,僅僅只能允許其中一個線程對其執行初始化操作,其餘線程必須等待,只有在活動線程執行完對類的初始化操作之後,纔會通知正在等待的其他線程。
p.s: 如果一個類中沒有對靜態變量賦值也沒有靜態語句塊,那麼編譯器可以不爲這個類生成<client>()方法。

5.1 類初始化的6種時機

(1)爲一個類型創建一個新的對象實例時(比如new、反射、序列化)

(2)調用一個類型的靜態方法時(即在字節碼中執行invokestatic指令)

(3)調用一個類型或接口的靜態字段,或者對這些靜態字段執行賦值操作時(即在字節碼中,執行getstatic或者putstatic指令),不過用final修飾的靜態字段除外,它被初始化爲一個編譯時常量表達式

(4)調用JavaAPI中的反射方法時(比如調用java.lang.Class中的方法,或者java.lang.reflect包中其他類的方法)

(5)初始化一個類的派生類時(Java虛擬機規範明確要求初始化一個類時,它的超類必須提前完成初始化操作,接口例外)

(6)JVM啓動包含main方法的啓動類時。

*注意以下幾種情況不會執行類初始化:

  • 通過子類引用父類的靜態字段,只會觸發父類的初始化,而不會觸發子類的初始化。
  • 定義對象數組,不會觸發該類的初始化。
  • 常量在編譯期間會存入調用類的常量池中,本質上並沒有直接引用定義常量的類,不會觸發定義常量所在的類。
  • 通過類名獲取Class對象,不會觸發類的初始化。
  • 通過Class.forName加載指定類時,如果指定參數initialize爲false時,也不會觸發類初始化,其實這個參數是告訴虛擬機,是否要對類進行初始化。
  • 通過ClassLoader默認的loadClass方法,也不會觸發初始化動作。

6.使用

                     略

7.卸載

                     略

參考文章:

   https://blog.csdn.net/zhangjg_blog/article/details/21486985

   https://blog.csdn.net/qq_39327985/article/details/81504646

   http://www.importnew.com/25295.html

   https://segmentfault.com/a/1190000004597758

   https://blog.csdn.net/u012585964/article/details/52011138/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章