1. 程式人生 > >深入理解JAVA虛擬機器(四):虛擬機器類載入機制

深入理解JAVA虛擬機器(四):虛擬機器類載入機制

虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這就是虛擬機器的類載入機制。在Java語言裡面,型別的載入、連線和初始化過程都是在程式執行期間完成的。

1、類載入的時機

類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期如下圖所示:

                          

載入、驗證、準備、初始化和解除安裝這5個階段的順序是確定的,類的載入過程必須按照這種順序開始,而解析階段則不一定:在某些情況下可以在初始化階段之後再開始,這是為了支援Java語言的執行時繫結。

Java虛擬機器規範中沒有強制約束什麼時候開始類載入的第一階段,而Java虛擬機器規範嚴格規定了有且只有5種情況必須立即對類進行“初始化”(載入、驗證、準備需要在此之前開始):

(1)遇到new、getstatic、putstatic或者invokestatic這4條位元組碼指令時,如果類沒有進行初始化,則需要先觸發其初始化。生成這個4條指令最常見的的場景是:使用new關鍵字例項化物件、讀取或設定一個類的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)、以及呼叫一個類的靜態方法。

(2)使用java.lang.reflect包的方法對類進行反射呼叫時,如果類沒有進行過初始化,則先進行初始化。

(3)當初始化一個類時,其父類還沒有進行初始化,則先觸發父類的初始化。

(4)當虛擬機器啟動時,使用者要指定一個執行的主類(包含main()方法的類),虛擬機器會先初始化這個主類。

(5)當使用JDK1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制代碼,並且這個方法控制代碼所對應的類沒有進行初始化,則需要先進行初始化。

這5種場景下對類的引用稱為主動引用,除此之外的所有引用類的方式都是被動引用,都不會觸發初始化。以下是被動引用的例子:

例項1:通過子類引用父類的靜態欄位,不會導致子類初始化。對於靜態欄位只有直接定義這個欄位的類才會被初始化。

public class SuperClass{
    static{
        System.out.println("SuperClass init!");
    }
    public static int value = 123;

}

public class SubClass extends SuperClass{
    static{
        System.out.println("SubClass init!");
    }
}

public class Test1{
    public static void main(String args[]){
        System.out.println(SubClass.value);
    }
}

上述程式碼會輸出"SuperClass init!"

例項2:通過陣列定義來引用類,不會觸發此類的初始化

public class SuperClass{
    static{
        System.out.println("SuperClass init!");
    }
    public static int value = 123;

}

public class SubClass extends SuperClass{
    static{
        System.out.println("SubClass init!");
    }
}

public class Test2{
    public static void main(String args[]){
        SuperClass[] sca = new SuperClass[10];
    }
}

執行後並不會輸出"SuperClass init!",說明並沒有觸發類SuperClass的初始化階段。

例項3:靜態常量在編譯階段會存入呼叫類的常量池中,引用靜態常量並不會直接引用到定義該靜態常量的類,不會觸發初始化。

public class ConstClass{
    static{
         System.out.println("ConstClass init!");
    }
    public static final String HELLOWORLD = "hello world";
}

public class Test3{
    public static void main(String[] args){
        System.out.println(ConstClass.HELLOWORLD);
    }
}

介面也有初始化過程,但是介面中不能使用static{}語句塊,但編譯器仍會為介面生成“<clinit>”類構造器,用於初始化介面中所定義的成員變數。介面與類初始化的區別還有:當一個類在初始化時,要求其父類全部都已經初始化,但介面初始化時並不要求其父介面全部都完成了初始化,只有在真正使用父介面的時候才會初始化。

2、類載入的過程

2.1 載入

在載入階段虛擬機器要完成3件事情:
(1)通過一個類的全限定名來獲取定義此類的二進位制位元組流。
(2)將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構。然後在記憶體中例項化一個java.lang.Class類的物件,這個物件將作為程式訪問方法區中的這些型別資料的外部介面。
(3)在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口。

一個非陣列類的載入階段既可以使用系統提供的引導類載入器來完成,也可以由使用者自定義的類載入器去完成。

對於陣列類,本身並不通過類載入器建立,它由Java虛擬機器直接建立的。但是陣列類的元素型別最終要靠類載入器去建立,一個數組類的建立過程遵循以下規則:
A、如果陣列的元件型別是引用型別,那就遞迴採用普通類的載入過程去載入這個元件型別,陣列C將在載入該元件型別的類載入器的類名稱空間上被標識。
B、如果陣列的元件型別不是引用型別,Java虛擬機器將會把陣列C標記為引導類載入器關聯。
C、陣列類的可見性與它的元件型別的可見性一致,如果元件型別不是引用型別,那陣列類的可見性將預設為public。
注意:載入階段和連線階段的部分內容是交叉進行的,載入階段尚未完成,連線階段可能已經開始。

2.2驗證

驗證是連線階段的第一步,這一階段的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。驗證階段大致會完成下面4個階段的檢驗動作:檔案格式驗證、元資料驗證、位元組碼驗證、符號引用驗證。
(1)檔案格式驗證
主要目的是保證輸入的位元組流能正確地解析並存儲與方法區之中,格式上符合描述一個Java型別資訊的要求。比如:要驗證位元組流是否符合Class檔案格式的規範,並且能被當前版本的虛擬機器處理,常量池中的常量是否有不被支援的常量型別,指向常量的各種索引值中是否有指向不存在的常量或不符合UTF8編碼的資料等等。
(2)元資料驗證
對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合Java語言規範的要求。比如:這個類是否有父類,是否繼承了不允許被繼承的類,如果這個類不是抽象類,是否實現了其父類或者介面中要求實現的所有方法等等。
(3)位元組碼驗證
通過資料流和控制流分析,確定程式語義是合法的、符合邏輯的。比如:保證任意時刻運算元棧的資料型別與指令程式碼序列都能配合工作,保證跳轉指令不會跳轉到方法體以外的位元組碼指令上,保證方法體中的型別轉換是有效的等等。
(4)符號引用校驗
此校驗發生在虛擬機器將符號引用轉化為直接引用的時候,這個轉化動作將在連線的第三階段——解析階段發生。符號引用校驗可以看作是對類自身以外(常量池中的各種符號引用)的資訊進行匹配性校驗。比如:符號引用中通過字串描述的全限定名是否能找到對應的類,在指定類中是否存在符合方法的欄位描述符以及簡單名稱所描述的方法和欄位等等。

2.3準備

準備階段正式為類變數分配記憶體並設定類變數初始值,這些變數所使用的記憶體都將在方法中進行分配。
注意:這個階段進行記憶體分配的僅包括類變數(被static修飾的變數),而不包括例項變數,例項變數將會在物件例項化時隨物件一起分配在Java堆中。這裡的初始值是資料型別的零值。比如:public static int value = 123;變數value在準備階段後的初始值是0,而把value賦值為123的動作將在初始化階段才會執行。如果類欄位的欄位屬性表中存在ConstantValue屬性,那在準備階段變數value就會初始化為ConstantlyValu額屬性所指定的值。

2.4解析

解析階段是虛擬機器講常量池內的符號引用替換為直接引用的過程。
符號引用:符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義定位到目標即可,符號引用與虛擬機器實現的記憶體佈局無關,引用的目標並不一定已經載入到記憶體中。
直接引用:直接引用可以是直接指向目標的指標、相對偏移量或者是一個能間接定位到目標的控制代碼。直接引用是和虛擬機器實現的記憶體佈局相關的,如果有了直接引用,那引用的目標必定已經在記憶體中存在了。
虛擬機器規範中要求用於操作符號引用的位元組碼指令之前,先對它們所使用的符號引用進行解析。所以虛擬機器需要根據需要判斷到底是在類被載入時對常量池中的符號引用進行解析,還是等到符號引用將要被使用前才去解析它。

類和介面的解析

比如在D類中把解析符號引用N解析為介面C的直接引用,步驟如下:
(1)如果C不是一個數組型別,那虛擬機器將會把代表N的全限定名傳遞給D的類載入器去載入C,在載入過程中,由於元資料驗證、位元組碼驗證的需要,又會觸發其他相關類的載入動作。
(2)如果C是一個數組型別,並且陣列元素為物件,那將會按照(1)的規則載入陣列元素型別,接著由虛擬機器生成一個代表此陣列維度和元素的陣列物件。
(3)如果面步驟沒有異常,那C在虛擬機器中已經成為一個有效的類或介面了,但解析完之前還要進行符號引用驗證,確認D是否具備對C的訪問許可權。

欄位解析

(1)首先會對欄位表內class_index項中索引的CONSTANT_Class_info字元引用進行解析,也就是欄位所屬的類或介面的符號引用,成功的話,用C代表這個類或介面。
(2)如果C本身包含了簡單名稱和欄位描述符都與目標匹配的欄位。則返回這個欄位的直接引用。
(3)否則,如果在C中實現了介面,就按照繼承關係從下往上遞迴搜尋各個介面和他的父介面,找到則返回。
(4)否則,將會按照繼承關係從下往上遞迴搜尋其父類,找到則返回。
(5)否則,查詢失敗。
查詢成功後要對這個欄位進行許可權驗證,不具備訪問許可權的話要丟擲異常。
如果在一個同名欄位同時出現在C的介面和父類中,那編譯器可能拒絕編譯。

類方法解析

(1)首先會解析出類方法表的class_index項中索引的方法所屬的類或者介面的符號引用,成功的話,用C代表這個類或介面。
(2)類方法和介面方法符號引用的常量型別定義是分開的,如果類方法表中發現class_index中索引的C是個介面,丟擲異常。通過檢查後,在類C中查詢是否有簡單名稱金額描述符都與目標相匹配的方法,如果有返回這個方法的直接引用。
(3)否則,在類C的父類中遞迴查詢,有的話返回。
(4)否則,在類C實現的介面列表及它們的父介面中遞迴查詢,存在的話,說明C是一個抽象類,丟擲異常。
(5)否則,查詢失敗,丟擲異常。

介面方法解析

(1)首先會解析出介面方法表的class_index項中索引的方法所屬的類或者介面的符號引用,成功的話,用C代表這個類或介面。
(2)如果在介面方法表中發現class_index中索引的C是個類,丟擲異常。通過檢查後,在類C中查詢是否有簡單名稱金額描述符都與目標相匹配的方法,如果有返回這個方法的直接引用。
(3)否則,在介面C中查詢,有的話返回。
(4)否則,在介面C的父介面中遞迴查詢,有的話返回。
(5)否則,查詢失敗,丟擲異常。

2.5初始化

初始化階段才真正開始執行類中定義的Java程式程式碼,在這一階段執行類構造器<clinit>方法。
<clinit>方法由編譯器自動收集類中的所有類變數的複製動作和靜態語句塊(static{}塊)中的語句合併產生的,順序由語句在原始檔中出現的順序決定。靜態語句塊中只能訪問到定義在靜態語句塊之前的變數,定義在它之後的變數,在前面的靜態語句塊只能賦值不能訪問。
虛擬機器會保證在子類<clinit>()方法執行前,父類的<clinit>()方法已經執行完畢。所以父類中定義的靜態語句塊要優先於子類的變數賦值操作。
在沒有靜態語句塊也沒有對靜態變數的賦值操作的類中不會有<clinit>方法
執行介面的<clinit>()方法不需要先執行父介面的<clinit>()方法。只有當父介面中定義的變數使用時,父接口才會初始化。介面的實現類在初始化時也不會執行介面的<clinit>()方法。

3、類載入器

對於任意一個類,都需要由載入它的類載入器和這個類本身一同確立其在java虛擬機器中的唯一性。
絕大部分Java程式會使用到以下3中系統提供的類載入器:
(1)啟動類載入器:使用C++語言實現,是虛擬機器自身的一部分,負責將存放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath引數所指定的路徑中的,並且是虛擬機器識別的類庫載入到虛擬機器記憶體中,無法被Java程式直接引用。
(2)擴充套件類載入器:負責載入<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫,開發者可以直接使用。
(3)應用程式類載入器:負責載入使用者類路徑上所指定的類庫,開發者可以直接使用,如果應用程式中沒有自定義過自己的類載入器,一般程式中預設使用此載入器。

雙親委派模型的工作過程是:如果一個類載入器收到了類載入的請求,它首先不會自己去載入這個類,而把這個請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入器請求最終都應傳送到頂層的啟動類載入器中,只有當父載入器無法完成這個載入請求時,子載入器才會嘗試自己去載入。如下圖是雙親委派模型的層次關係:
                                        

使用雙親委派模型的好處是Java類隨著它的載入器一起具備了一種帶有優先順序的層次關係。例如類java.lang.Object,它存放在rt.jar中,無論哪一個類載入器載入它,最後都會委派給啟動類載入器,因此可以保證Object類在程式的各種類載入器環境中都是同一個類。

相關推薦

深入理解JAVA虛擬機器虛擬機器載入機制

虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這就是虛擬機器的類載入機制。在Java語言裡面,型別的載入、連線和初始化過程都是在程式執行期間完成的。 1、類載入的時機 類從被載入到虛擬機

深入理解Java記憶體模型——volatile

volatile的特性 當我們宣告共享變數為volatile後,對這個變數的讀/寫將會很特別。理解volatile特性的一個好方法是:把對volatile變數的單個讀/寫,看成是使用同一個鎖對這些單個讀/寫操作做了同步。下面我們通過具體的示例來說明,請看下面的示例程式碼: class Vol

深入理解Java的註解Annotation註解處理器3

isp 通過反射 out peid 擴展 .cn 自定義註解 忽略 否則 如果沒有用來讀取註解的方法和工作,那麽註解也就不會比註釋更有用處了。使用註解的過程中,很重要的一部分就是創建於使用註解處理器。Java SE5擴展了反射機制的API,以幫助程序員快速的構造自定義註解處

深入理解jvm虛擬機器位元組碼執行引擎

執行時棧幀 每一個方法從呼叫開始到執行完成都對應著一張棧幀的進棧和出棧。棧幀中儲存著區域性變量表,運算元表,動態連結和方法返回地址。位於虛擬機器最頂層的稱為當前方法棧。 區域性變量表 儲存當前方法的區域性變數和引數,區域性變量表的容量以變數槽slo

Java程式設計師從笨鳥到菜鳥之九十九深入java虛擬機器開發自己的載入

                歡迎閱讀本專題的其他部落格:          在大多數情況下,系統預設提供的類載入器實現已經可以滿足需求。但是在某些情況下,您還是需要為應用開發出自己的類載入器。比如您的應用通過網路來傳輸 Java 類的位元組程式碼,為了保證安全性,這些位元組程式碼經過了加密處理。這個時候您

Java虛擬機器Class檔案結構及位元組碼指令

    接下來的兩個位元組為this_class項,它是一個對常量池的索引。在this_class位置的常量池入口必須為CONSTANT_Class_info表。該表由兩個部分組成——標籤和name_index。標籤部分是一個具有CONSTANT_Class值的常量,在name_index位置的常量池入口為一

深入理解多執行緒—— Moniter的實現原理

在深入理解多執行緒(一)——Synchronized的實現原理中介紹過關於Synchronize的實現原理,無論是同步方法還是同步程式碼塊,無論是ACC_SYNCHRONIZED還是monitorenter、monitorexit都是基於Monitor實現的,那麼這篇來介紹下什麼是Monitor。

深入理解Java記憶體模型——final

與前面介紹的鎖和volatile相比較,對final域的讀和寫更像是普通的變數訪問。對於final域,編譯器和處理器要遵守兩個重排序規則: 在建構函式內對一個final域的寫入,與隨後把這個被構造物件的引用賦值給一個引用變數,這兩個操作之間不能重排序。 初次讀一個包含

深入理解Java記憶體模型——基礎

併發程式設計模型的分類 在併發程式設計中,我們需要處理兩個關鍵問題:執行緒之間如何通訊及執行緒之間如何同步(這裡的執行緒是指併發執行的活動實體)。通訊是指執行緒之間以何種機制來交換資訊。在指令式程式設計中,執行緒之間的通訊機制有兩種:共享記憶體和訊息傳遞。 在共享記憶體的併發模型裡,執行緒之

深入理解Java記憶體模型——順序一致性

資料競爭與順序一致性保證 當程式未正確同步時,就會存在資料競爭。java記憶體模型規範對資料競爭的定義如下: 在一個執行緒中寫一個變數, 在另一個執行緒讀同一個變數, 而且寫和讀沒有通過同步來排序。 當代碼中包含資料競爭時,程式的執行往往產生違反直覺的結果(前一章的示例正是如此)。如果一

深入理解Java記憶體模型——重排序

資料依賴性 如果兩個操作訪問同一個變數,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在資料依賴性。資料依賴分下列三種類型: 名稱 程式碼示例 說明 寫後讀 a = 1;b = a; 寫一個變數之後,再讀這個位置。 寫後寫 a = 1;a = 2; 寫一個變數之後,再寫這

深入理解Java記憶體模型——總結

處理器記憶體模型 順序一致性記憶體模型是一個理論參考模型,JMM和處理器記憶體模型在設計時通常會把順序一致性記憶體模型作為參照。JMM和處理器記憶體模型在設計時會對順序一致性模型做一些放鬆,因為如果完全按照順序一致性模型來實現處理器和JMM,那麼很多的處理器和編譯器優化都要被禁止,這對執行效能

深入理解Java記憶體模型——鎖

鎖的釋放-獲取建立的happens before 關係 鎖是java併發程式設計中最重要的同步機制。鎖除了讓臨界區互斥執行外,還可以讓釋放鎖的執行緒向獲取同一個鎖的執行緒傳送訊息。下面是鎖釋放-獲取的示例程式碼: class MonitorExample { int a = 0;

深入理解德語動詞變化

這一篇是本系列的完結篇。在本篇中將探討語態及語式對動詞變化的影響,並引申出另外的一個專題“深入理解德語動詞的分類與構詞”。 ­ 接續前文,另外兩個影響德語動詞變化的重要因素就是語態(Genera Verbi)和語式(Modicus)。語態的概念對我們來說並不陌生,只是我們的

java學習筆記import語法

employee sign cnblogs java 調用 變量賦值 temp 職位 求職 Import 語法是給編譯器尋找特定類的適當位置的一種方法。 創建一個Employee 類,包括四個實體變量姓名(name),年齡(age),職位(designation)和薪水(s

Java虛擬JVM的運行機制

包括 MQ inf 分配 font 可能 mage star 都是 一、JVM啟動流程 1.java虛擬機啟動的命令是通過java +xxx(類名,這個類中要有main方法)或者javaw啟動的。 2.執行命令後,系統第一步做的就是裝載配置,會在當前路徑中尋找jvm的

深入理解MyBatis的原理配置文件上

dynamic 如何 turn ready conf 屬性。 支持 left bool 前言:前文提到一個入門的demo,從這裏開始,會了解深入 MyBatis 的配置,本文講解 MyBatis 的配置文件的用法。 目錄 1、properties 元素 2、設置(set

深入理解MyBatis的原理配置文件用法

pac amt 單個 gis obb rri tab obj 用戶 前言:前文講解了 MyBatis 的配置文件一部分用法,本文將繼續講解 MyBatis 的配置文件的用法。 目錄 1、typeHandler 類型處理器 2、ObjectFactory 3、插件 4、e

Java並發volatile的實現原理

ont style tile 讀寫 flush microsoft div 圖片 println synchronized是一個重量級的鎖,volatile通常被比喻成輕量級的synchronized volatile是一個變量修飾符,只能用來修飾變量。 volatile寫

深入理解線性迴歸演算法正則項的詳細分析

前言 當模型的複雜度達到一定程度時,則模型處於過擬合狀態,類似這種意思相信大家看到個很多次了,本文首先討論了怎麼去理解複雜度這一概念,然後回顧貝葉斯思想(原諒我有點囉嗦),並從貝葉斯的角度去理解正則項的含義以及正則項降低模型複雜度的方法,最後總結全文。     &nb