JVM虛擬機器和類載入器

taozihk發表於2016-05-17

類載入器深入剖析

Java虛擬機器與程式的生命週期

1) 執行了System.exit()方法

2) 程式正常執行結束

3) 程式在執行過程中遇到了異常或錯誤而異常終止

4) 由於作業系統出現錯誤而導致java虛擬機器程式終止

類的載入、連線與初始化

  • 載入:查詢並載入類的二進位制資料
  • 連線

           1) 驗證:確保被載入的類的正確性

           2) 準備:為類的靜態變數分配記憶體,並將其初始為預設值

           3) 解析把類中的符號引用轉換為直接引用

  •  初始化:為類的靜態變數賦予正確的初始值

Java程式對類的使用方式分為兩種

1) 主動使用

2) 被動使用

 

所有的Java虛擬機器實現必須在每個類或介面被Java程式

首次主動呼叫”時才初始化它們

 

主動使用(六種)

  1. 建立類的例項
  2. 訪問某個類或介面的靜態變數,或者對該靜態變數賦值
  3. 呼叫類的靜態方法
  4. 反射(如Class.forName("com.lang.String")
  5. 初始化一個類的子類
  6.  Java虛擬機器啟動時被標為啟動類的類


 除了以上六種情況,其它使用Java類的方式都被看作是對類的被動使用,都不會導致類的初始化

 

類的載入指的是將類的.class檔案中的二進位制資料讀入到記憶體中,將其放在執行時資料區的方法區內,

然後再堆區建立一個java.lang.Class物件,用來封裝類在方法區內的資料結構

類的載入

載入.class檔案的方式

  • 從本地系統中直接載入
  • 通過網路下載.class檔案
  • zipjar等歸納檔案中載入.class檔案
  • 從專有資料庫中提取.class檔案
  • java原始檔動態編譯為.class檔案

 

類的載入的最終產品是位於堆區中的Class物件

 

Class物件封裝了類在方法區內的資料結構,並向Java程式設計師提供了訪問方法區內的資料結構的介面

 

有兩種型別的類載入器

(一)Java虛擬機器自帶的載入器

  • 根載入器(Bootstrap
  • 擴充套件類載入器(Extension)
  • 系統類載入器(System

(二)使用者自定義的類載入器

  • java.lang.ClassLoader的子類
  • 使用者可以定製類的載入方式

 

類載入器並不需要等到某個類被“首次使用”時再載入它


VM規範允許類載入器在預料某個類被使用時就預先載入它,如果存在預先載入的過程中遇到了.class檔案缺失或存在錯誤,

類載入器必須在程式首次主動使用該類時才報告錯誤(LinkageError錯誤)

 

如果這個類一直沒有被程式主動使用,那麼類載入器就不會報告錯誤

 

類被載入後,就進入連線階段。將已經讀入到記憶體的類的二進位制資料合併到虛擬機器的執行時環境中去。


類的驗證

 類的驗證的內容

  • 類檔案的結構檢查
  • 語義檢查
  • 位元組碼驗證
  • 二進位制相容性的驗證
類的驗證主要包括以下內容。
  • 類檔案的結構檢查:確保類檔案遵從Java類檔案的固定格式。

  • 語義檢查:確保類本身符合Java語言的語法規定,比如驗證final型別的類沒有子類,以及final型別的方法沒有被覆蓋。

  • 位元組碼驗證:確保位元組碼流可以被Java虛擬機器安全的執行,位元組碼流代表Java方法(包括靜態方法和例項方法),它是由被稱作操作碼的單位元組指令組成的序列,每一個操作碼後都跟著一個或多個運算元,位元組碼驗證步驟會檢查每個操作碼是否合法,即是否有著合法的運算元。

  • 二進位制相容的驗證:確保相互引用的類之間協調一致,例如在Worker類的goto方法中會呼叫Car類的run方法,java虛擬機器在驗證Worker類時,會檢查在方法區內是否存在Car類的run方法,假如不存在,會丟擲NoSuchMethodError錯誤。

類的準備

在準備階段,Java虛擬機器為類的靜態變數分配記憶體,並設定預設的初始值。

例如,對於以下Sample類,在準備階段,將為int型別的靜態變數a分配4個位元組的記憶體空間,並且賦予預設值0,為long型別的靜態變數b分配8個位元組的記憶體空間,並且賦予預設值0

  
<pre name="code" class="java">public calss Sample{
    private static int a = 1;
    public static long b;
    static{
      B = 2;
    }
    ...
}


類的解析

在解析階段,java虛擬機器會把類的二進位制資料中的符號引用替換為直接引用,例如在Worker的類的gotoWork()方法中會引用Car類的run()方法。

 

<pre name="code" class="java">public void gotowork()
    car.run();//這段程式碼在Worker類的二進位制資料中表示為符號引用
}


在Worker類的二進位制資料中,包含了一個隊Car類的run()方法的符號引用,它由run()方法的全名和相關描述符組成,在解析階段,Java虛擬機器會把這個

符號引用替換為一個指標,該指標指向Car類的run()方法在方法區內的記憶體位置,這個指標就是直接引用。

類的初始化

在初始化階段,Java虛擬機器執行類的初始化語句,為類的靜態變數賦予初始值。

在程式中,靜態變數的初始化有兩種途徑:(1)在靜態變數的宣告處進行初始化;(2)在靜態程式碼快中進行初始化。例如在以下程式碼塊中,靜態變數ab都被顯示初始化,而靜態變數c沒有被顯示初始化,它將保持預設值0.

 

</pre><pre name="code" class="java">public class Sample{
    private int static int a = 1;//在靜態變數的宣告處進行初始化
    public static long b;
    public staitc long c;
    static {
      b=2//在靜態程式碼塊中進行初始化
    }
    ....
}

靜態變數的宣告語句中,以及靜態程式碼塊都被看做類的初始化語句,Jva虛擬機器按照初始化語句在類檔案中的先後順序來依次執行它們。

例如當以下Sample類被初始化後,它的靜態變數a的取值為4.

 

<pre name="code" class="java">public class Sample{
    static int a = 1;
    static{a = 2;}
    static{a = 4;}
    public static void main(String args[]){
     System.out.println(“a=”+a);//列印a=4
    }
}



 類的初始化步驟

  1. 假如這個類還沒有被載入和連線,那就先進行載入和連線。
  2. 假如類存在直接的父類,並且這個父類還沒有被初始化,那就先初始化直接的父類
  3. 假如類中存在初始化語句,那就依次執行這些初始化語句

類的初始化時機

Java虛擬機器初始化一個類時,要求它的所有父類都已經被初始化,但是這條規則並不適用於介面。

  • 在初始化一個類時,並不會先初始化它所實現的介面。
  • 在初始化一個介面時,並不會先初始化它的父介面。

因此,一個父介面並不會因為它的子介面或者實現類的初始化而初始化,只有當程式首次使用特定介面的靜態變數時,才會導致該介面的初始化。

 

 只有當程式訪問的靜態變數或靜態方法確實在當前類或當前介面中定義時,才可以認為是對類或介面的主動使用。

 

呼叫ClassLoader類的loadClass方法載入一個類,並不是對類的主動使用,不會導致類的初始化。


類載入器

類載入器用來把類載入到Java虛擬機器中。從JDK1.2版本開始,類的載入過程採用父親委託機制,這種機制能更好的保證Java平臺的安全,。在此委託機制中,除了

Java虛擬機器自帶的根類載入器以為,其餘的類載入器都有且只有一個父載入器。當Java程式請求載入器loader1載入Sample類時,loader1首先委託自己的父載入器

去載入Sample類,若父載入器能載入,則由父載入器完成載入任務,否則才由載入器loader1本身載入Sample類。

  • 根(Bootstrap)類載入器:該載入器沒有父載入器。它負責家在虛擬機器的核心類庫,如java.lang.*等。Java.lang.Object就是由根類載入器載入的。根類載入器從系統屬性sun.boot.class.path所指定的目錄中載入類庫。根類載入器的實現依賴於底層作業系統,屬於虛擬機器的實現的一部分,它沒有繼承java.lang.ClassLoader類。

  • 擴充套件(Extension)類載入器:它的父載入器為根類載入器。它從java.ext.dirs系統屬性所指定的目錄中載入類庫,或者從JDK的安裝目錄的jre\lib\ext子目錄(擴充套件目錄)下載入類庫。如果使用者建立的jar檔案放在這個目錄下,也會自動由擴充套件類載入器載入。擴充套件類載入器是純JAVA類,是java.lang.ClassLoader類的子類。

  • 系統(System)類載入器:也稱為應用類載入器,它的父載入器為擴充套件類載入器。它從環境變數classpath或者系統屬性java.class.path所指定的目錄中載入類,它是使用者自定義的類載入器的預設父載入器。系統類載入器是純JAVA類,是java.lang.ClassLoader類的子類。

除了以上虛擬機器自帶的載入器以外,使用者還可以定製自己的類載入器

User-defined Class Loader)Java提供了抽象類java.lang.ClassLoader,

所有使用者自定義的類載入器應該繼承ClassLoader


類載入器的父委託機制

在父親委託機制中,各個載入器按照父子關係形成了樹形結構,

除了根載入器以外,其餘的類載入器都有且只有一個父載入器。



Class sampleClass = loader2.loadClass(“Sample”);

    loader2首先從自己的名稱空間中查詢Sample類是否已經被載入,如果已經載入,直接返回代表Sample類的Class物件的引用。

    如果Sample類還沒有被載入,loader2首先請求loader1代為載入,loader1再請求系統類載入器代為載入,系統類載入器再請求擴充套件類載入器代為載入,擴充套件類載入器再請求根類載入器代為載入。若根類載入器和擴充套件類載入器都不能載入,則系統類載入器嘗試載入,若能載入成功,則將Sample類所對應的Class物件的引用返回給loader1loader1再將引用返回給loader2,從而成功將Sample類載入進虛擬機器。若系統類載入器不能載入Sample類,則loader1嘗試載入Sample類,若loader1也不能成功載入,則loader2嘗試載入。若所有的父載入器及loader2本身都不能載入,則丟擲ClassNotFoundException異常。


若有一個類載入能成功載入Sample類,那麼這個類載入器被稱為定義類載入器,所有能成功返回Class物件的引用的類載入器(包括定義類載入器)都被稱為初始類載入器。

假設:

Loader1實際載入了Sample類,則loader1Sample類的定義類載入器,loader2loader1Sample類的初始類載入器。


需要指出的是,載入器之間的父子關係實際上指的是載入器物件之間的包裝關係,而不是類之間的繼承關係。一對父子載入器可能是同一個載入器類的兩個例項,也可能不是。在子類載入器物件中包裝了一個父載入器物件。例如以下loader1loader2MyClassLoader類的例項,並且loader2包裝了loader1loader1loader2的父載入器。

<pre name="code" class="java">ClassLoader loader1 = new MyClassLoader();
//引數loader1將作為loader2的父載入器
ClassLoader loader2 = new MyClassLoader(loader1);


父親委託機制的優點是能夠提高軟體系統的安全性。因為在此機制下,使用者自定義的類載入器不可能載入應該由父載入器載入的可靠類,從而防止不可靠甚至惡意的程式碼代替父載入器載入的可靠程式碼。例如,java.lang.Object類總是由根類載入器載入,其它任何使用者自定義的類載入器都不可能載入含有惡意程式碼的java.lang.Object類。

名稱空間

每個類載入器都有自己的名稱空間,名稱空間由該載入器及所有父載入器所載入的類組成。在同一個名稱空間中,不會出現類的完整名字(包括類的包名)相同的兩個類;在不同的名稱空間中,有可能會出現類的完整名字(包括類的包名)相同的兩個類

執行時包

由同一類載入器載入的屬於相同包的類組成了執行時包。決定兩個類是不是屬於同一個執行包,不僅要看它們的包名是否相同,還要看定義類載入器是否相同。只有屬於同一執行時包的類才能互相訪問包可見(即預設訪問級別)的類和類成員。這樣的限制能避免使用者自定義的類冒充核心類庫的類,去訪問核心類庫的包可見成員。假如使用者自定義了一個類java.lang.Spy,並由使用者自定義的類載入器載入,由於java.lang.Spy和核心類庫java.lang.*由不同載入器載入,它們屬於不同的執行時包,所以java.lang.Spy不能訪問核心類庫java.lang包中的包可見成員。


建立使用者自定義的類載入器

要建立使用者自己的類載入器,只需要擴充套件java.lang.ClassLoader類,然後覆蓋它的findClassString name)方法即可,該方法根據引數指定的類的名字,返回對應的Class物件的引用。


當執行loader2.loadClass(“Sample”)時,先由它上層的所有父載入器嘗試載入Sample類。Loader1d:\myapp\serverlib目錄下成功的載入了Sample類,因此loader1Sample類的定義類載入器,loader1loader2Sample類的初始類載入器。

當執行loader3.loadClass(“Sample”)時,先由它上層所有父載入器嘗試載入Sample類。Loader3的父載入器為根類載入器,它無法載入Sample類,接著loader3d:\myapp\serverlib目錄下成功的載入了Sample類,因此loader3Sample類的定義類載入器及初始類載入器。

相關文章