JVM虛擬機和類加載器

類加載器深入剖析

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類的定義類加載器及初始類加載器。

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