1. 程式人生 > >Java虛擬機器類載入的過程

Java虛擬機器類載入的過程

1. 類載入的時機:

  • 類從被載入到虛擬機器記憶體開始到卸載出記憶體,整個生命週期包括以下七個階段,其中載入,驗證,準備,初始化,解除安裝這5個階段的順序是確定的。
    類生命週期

  • 類在什麼情況下進行載入: 虛擬機器對類的載入時機並沒有明確的規定,是由具體的虛擬機器實現的,當時明確規定了,在以下5種情況下(有且只有),類必須進行初始化(載入、驗證、準備必須在初始化之前)。這5種方式也被稱為主動引用,除此之外的其他引用都不會觸發初始化,稱為被動引用。

    • 遇到new(使用new關鍵字建立例項物件的時候)、getstatic(讀取一個靜態變數,被final修飾,已在編譯把結果放入常量池的靜態變數除外)、putstatic(設定一個靜態變數,被final修飾,已在編譯把結果放入常量池的靜態變數除外)或invokestatic(呼叫一個類的靜態方法)這四條位元組碼指令的時候,如果類沒有進行初始化,則需要先觸發其初始化。
    • 使用java.lang.reflect方法對類進行反射呼叫的時候,如果類沒有進行初始化,則需要先觸發器初始化
    • 初始化一個類時,發現其父類沒有進行初始化,則需要先觸發父類的初始化。
    • 虛擬機器啟動的時候,使用者需要指定一個執行的主類(包含main方法的類),虛擬機器會先對初始化這個主類
    • 當使用JDK1.7的動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法的控制代碼,並且這個方法控制代碼所對應的類沒有進行過初始化,則需要先觸發其初始化。(使用場景未知)

2. 類載入的過程:

2.1 載入:

  • 載入過程中虛擬機器需要完成的幾件事:
    • 通過一個類的全限定名類獲取定義此類的二進位制位元組流–可從多種渠道獲取(jar,war,網路等)
    • 將這個位元組流所代表的靜態儲存結構轉換為方法區的執行時資料結構
    • 在記憶體中生成一個代表這個類的java.lang.Class物件,作為這個方法區這個類的各種資料的訪問入口(HotSpot虛擬機器將這個物件儲存在方法區中,該物件將作為程式訪問方法區中這些資料型別的外部介面)

2.2 驗證:確保Class檔案中的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。

  • 驗證階段大致會完成以下4個階段的檢驗動作:
    • 檔案格式驗證:位元組流是否符合Class檔案格式的規範,並且能被當前版本的虛擬機器處理。
      • 包括魔數校驗:是否以0xCAFEBABE開頭
      • 主次版本號是否在當前虛擬機器的處理範圍內
      • 。。。。
    • 元資料驗證:對位元組碼描述的資訊進行語義分析,保證其描述的資訊符合java語言規範的要求。
      • 這個類是否有父類
      • 這個類的弗雷是否繼承了不允許被繼承的類(被final修飾的類)
      • 如果這個類不是抽象類,是否實現了其父類或介面之中要求實現的所有方法
      • 類中的欄位,方法是否和父類產生矛盾(如覆蓋率弗雷的final欄位,或者出現不符合規則的方法的過載,例如方法引數都一致,但返回值型別卻不相同等)
    • 位元組碼驗證:通過資料流和控制流分析,確定程式的語義是否是合法的,符合邏輯的
      • 保證跳轉指令不會跳轉到方法體之外的位元組碼指令上
      • 保證方法體中的型別轉換是有效的,例如可以把子類物件賦值給父類資料型別,這是安全的,但是不能吧父類物件賦值給子類資料型別,甚至是毫不相關的型別
      • 保證任意時刻運算元棧的資料型別和指令碼序列都能配合工作。
    • 符號引用驗證:對類自身以外(常量池中的各種符號引用)的資訊進行匹配性校驗, 這一階段的校驗發生在虛擬機器將符號引用轉換為直接引用的時候,這個動作發生在連線的第三階段(解析階段)中發生。
      • 符號引用中通過字串描述的全限定名是否能找到相應的類
      • 在指定的類中是否存在符合方法的欄位描述符以及簡單名稱所描述的方法和欄位
      • 符號引用中的類、欄位、方法的訪問性(private、protected、public、default)是否可被當前類訪問

2.3 準備:正式為類變數分配記憶體並設定類變數初始值的階段,這些變數所使用的記憶體都將在方法區中進行分配

  • 這個時候進行記憶體分配的只包括類變數(被static修飾的變數),並不包括例項變數,例項變數是在物件例項化時隨物件一起分配在java堆中。
  • 分配的初始值為零值,假設一個變數定義為:public static int value = 123;則設定變數的初始值應該為0, 而不是123. 把value賦值為123的putstatic指令是在程式被編譯後,存放於類構造器<clinit>()方法之中,所以吧value賦值為123的動作將在初始化階段才會執行。
  • 如果欄位屬性表中存在ConstantValue屬性,那麼在準備階段會將value賦值為ConstantValue屬性所指定的值
    • 例如:public static final int value = 123; 那麼在準備階段,則會將value賦值為123;

2.4 解析 :將符號引用轉換成直接引用的過程

  • 符號引用和直接引用的定義:

    • 符號應用:符號引用是一組以符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用是能無歧義的定位到目標即可。符號引用與虛擬機器實現的記憶體佈局無關,引用的目標並不一定已經載入到記憶體中。各種虛擬機器實現的記憶體佈局可以各不相同,但是它們能接受的符號引用必須都是一致的,因為符號引用的字面量形式明確定義在java虛擬機器規範的Class檔案格式中。符號引用在Class檔案中以CONSTANT_Class_info(類或介面的符號引用)、CONSTANT_Fieldref_info(欄位的符號引用)、CONSTANT_Methodref_info(方法的符號引用)等型別的常量出現。

      符號引用主要包括以下三類常量:

      • 類和介面的全限定名:java.lang.String的全限定名為:java/lang/String
      • 欄位的名稱和描述符:java.lang.String[][]二維陣列的描述符為:[[Ljava.lang.String
      • 方法的名稱和描述符:java.lang.String.toString()方法的描述符為:()Ljava.lang.String
    • 直接引用:直接引用可以是直接指向目標的指標、相對偏移量或是一個能間接定位到目標的控制代碼。直接引用是和虛擬機器實現的記憶體佈局相關的,同一個符號引用在不同虛擬機器例項上翻譯出來的直接引用一般不會相同。如果有了直接應用,那引用的目標必定已經在記憶體中存在
  • 解析階段的發生時間:

    • 虛擬機器規範中並沒有規定解析階段發生的具體時間,值要求類在執行:anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield、putstatic這16個用於操作符號引用的位元組碼指令之前,先對它們所使用的符號引用進行解析。
  • 解析針對的符號引用型別:
    • 解析動作主要針對:類或介面(CONSTANT_Class_info)、欄位(CONSTANT_Fieldref_info)、類方法(CONSTANT_Methodref_info)、介面方法(CONSTANT_InterfaceMethodref_info)、方法型別(CONSTANT_MethodType_info)、方法控制代碼(CONSTANT_MethodHandle_info)和呼叫點限定符(CONSTANT_InvokeDynamic_info)7類符號引用進行。(CONSTANT_String_info也需要解析過程)
  • 解析符號引用過程分析:

    • 類或介面的解析(CONSTANT_Class_info)
      • 假設當前程式碼所處的類為D,如果要吧一個未解析過的符號引用N解析為一個類或介面C的符號引用,虛擬機器需要完成以下3個步驟:
        1. 如果C不是一個數字型別,那虛擬機器將會吧代表N的全限定名傳遞個D的類載入器去載入這個類C。
        2. 如果C是一個數組型別?
        3. 如果載入沒有異常,那麼C在虛擬機器中實際已經成為一個有效的類或介面了,但在解析完成之前還需要進行符號引用的校驗,確認D是否具備對C的訪問許可權。如果不具備會丟擲java.lang.IllegalAccessError異常。
    • 欄位解析:解析一個未被解析過的欄位符號引用,首先會對欄位所屬的類或介面的符號引用進行解析。如果解析成功,則將這個欄位所屬的類或介面用C表示,虛擬機器規範要求安裝如下步驟對C進行後續欄位的搜尋:

      • 如果C本身就寶航了簡單名稱和欄位描述符都與目標相匹配的欄位,則返回這個欄位的直接引用,查詢結束。
      • 否則,如果在C中實現了介面,將會按照繼承關係從下往上遞迴搜尋各個介面和他的父介面,如果介面中包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則返回這個欄位的直接引用,查詢結束。
      • 否則:如果C不是java.lang.Object的話,將會按照繼承關係從下往上遞迴搜尋其弗雷,如果在父類中包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則返回這個欄位的位元組引用,查詢結束。
      • 否則查詢失敗,丟擲java.lang.NoSushFieldError異常。

      如果查詢過程中返回了引用,則需要對這個欄位進行許可權校驗,如果發現不具備對欄位的訪問許可權,將丟擲java.lang.IllegalAccessError異常。

    • 類方法解析:首先會對方法所屬的類或介面的符號引用進行解析。如果解析成功,則將這個方法所屬的類或介面用C表示,虛擬機器規範要求安裝如下步驟對C進行後續的類方法搜尋:

      • 類方法和介面方法符號引用的常量型別定義是分開的,如果在類方法表中發現class_index中索引的C是一個介面,那就直接丟擲:java.lang.IncompatibleClassChangeError異常
      • 如果通過了第一步,在類C中查詢是否有簡單名稱和描述符都與目標相匹配的方法,如果有這返回這個方法的直接引用,查詢結束。
      • 否則,在類C的父類中遞迴查詢是否有簡單名稱和描述符都與目標相匹配的方法,如果有,則返回這個方法的直接引用,查詢結束。
      • 否則,在類C實現的介面列表及它們的弗雷介面之中遞迴查詢是否有簡單名稱和描述符都與目標相匹配的方法,如果存在匹配法方法,說明類C是一個抽象類,這時查詢結束,丟擲java.lang.AbstractMethodError異常。
      • 否則,宣告方法查詢失敗,丟擲java.lang.NoSushMethodError

      最後,如果查詢過程成功放回了直接引用,將會對這個方法進行許可權驗證,如果發現不具備對此方法的訪問許可權,將丟擲java.lang.IllegalAccessError異常

    • 介面方法的解析:介面方法也需要先解析出介面方法表的class_index項中索引的方法所屬的類或介面的符號引用。如果解析成功,則將這個方法所屬的類或介面用C表示,虛擬機器規範要求安裝如下步驟對C進行後續的類方法搜尋:

      • 與類方法的解析不同,如果在介面方法中發現class_index中的索引C是個類而不是介面,那就直接丟擲java.lang.IncompatibleClassChangeError異常。
      • 否則,在介面C中查詢是否有簡單名稱和描述符都與目標相陪陪的方法,如果有則返回這個方法的直接引用,查詢結束。
      • 否則,在介面C的父介面中遞迴查詢,直到java.lang.Object類(查詢範圍會包含Object類)為止,看是否有簡單名稱和描述符都與目標相匹配的方法,如果有則返回這個方法的直接引用,查詢結束。
      • 否則,宣告查詢失敗,丟擲java.lang.NoSushMethodError異常。

      介面中的所有方法預設都是public的,所以不存在訪問許可權問題,因此介面方法的符號解析應當不會出現java.lang.IllegalAccessError異常

2.5 初始化:初始化階段才真正開始執行類中定義的java程式程式碼(位元組碼)

  • 在準備階段,變數已經賦過一次系統要求的初始值,而在初始化階段,則根據程式猿通過程式指定的主觀計劃去初始化類變數和其他資源,或者可以從另外一個角度來表達:初始化階段是執行類構造器<clinit>()方法的過程。

    <clinit>()方法解析:

    • <clinit>方法是有編譯器自動收集類中的所有類變數的賦值動作和靜態語句塊(static{}塊)中的語句合併產生的,編譯器手機的順序是由語句在原始檔中出現的順序決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變數,定義在他之後的變數,在前面的靜態語句塊可以賦值,但不能訪問:

      public class Test
      {
          static
          {
              i = 0;//給變數賦值可以正常通過
              System.out.println(i);//這句話會編譯提示”非法向前訪問“
          }
      }
      
    • <clinit>方法與類的構造器(或者說例項構造器<init>()方法)不同,它不需要顯示地呼叫父類構造器,虛擬機器會保證在子類的<clinit>()方法執行前,父類的<clinit>()方法已經執行完畢,因此在虛擬機器中的一個被執行的<clinit>()方法的類肯定是java.lang.Object。

    • 由於父類的<clinit>()方法先執行,也就意味著父類中定義的靜態語句塊要優先於子類的變數的操作。
    • <clinit>()方法對於類或介面來說並不是必需的,如果一個類中沒有靜態語句塊,也沒有對變數的賦值操作,那麼編譯器可以不為這個類生成<clinit>()方法。
    • 介面中不能使用靜態語句塊,但仍然有變數初始化的賦值操作,因此介面與類一樣都會生成<clinit>()方法,但介面與類不同的是,執行介面的<clinit>()方法不需要先執行父介面的<clinit>()方法。只有當父介面中定義的變數使用時,父接口才會初始化。另外,介面的實現類在初始化時也一樣不會執行介面的<clinit>()方法。
    • 虛擬機器會保證一個類的<clinit>()方法在多執行緒環境中被正確的加鎖、同步,如果多個執行緒同時去初始化一個類,那麼只會有一個執行緒去執行這個類的<clinit>()方法,其他執行緒都需要阻塞等待,知道活動執行緒執行<clinit>()方法完畢。如果在一個類的<clinit>()方法中有耗時很長的操作,就可能會造成多個程序阻塞,在實際應用中這種阻塞往往是很隱蔽的。

相關推薦

java虛擬機器載入過程(精簡版)

java虛擬機器類載入過程步驟:     1.載入 將虛擬機器外部的二進位制位元組流儲存到方法區中: a.獲取此類二進位制流: 通過一個類的全限定名來獲取定義此類的二進位制流; b.資料結構轉化: 將位元組流所代表的靜態儲存結

JAVA虛擬機器載入過程

什麼時候進行類載入 jvm虛擬機器規範沒有強制性的規定何時需要進行類的載入,但是如果遇到了以下幾種情況的指令則強制必須立即對類進行載入 new 建立物件的時候, getstatic 讀取靜態欄位的時候, putstatic 設定靜態欄位的時候, invokes

java虛擬機器載入過程記憶體情況底層原始碼分析及ClassLoader講解

讀書筆記加自我總結----------------------------------------------- 《瘋狂JAVAj講義》 《深入理解JAVA虛擬機器》第七章虛擬機器載入機制 《傳智播客Java底層公開課視訊》教學視訊 參考: 一、虛擬機器的類載入機制

Java虛擬機器載入過程

1. 類載入的時機: 類從被載入到虛擬機器記憶體開始到卸載出記憶體,整個生命週期包括以下七個階段,其中載入,驗證,準備,初始化,解除安裝這5個階段的順序是確定的。 類在什麼情況下進行載入: 虛

Java虛擬機器-載入器和載入過程

類載入器 java.lang.ClassLoader類及其子類可以讓java程式碼動態地載入到JVM中。每一個類都有載入它的ClassLoader的引用。每一個類載入器類都有一個載入它的父類載入器,類載入器的頂端稱為啟動類載入器(Bootstrap Class

java虛擬機器載入機制學習

1、什麼是類的載入 類的載入指的是將類的.class檔案中的二進位制資料讀入到記憶體中,將其放在執行時資料區的方法區內,然後在堆區建立一個java.lang.Class物件,用來封裝類在方法區內的資料結構。類的載入的最終產品是位於堆區中的Class物件,Class物件封裝了類在方法區內的資料結構

Java虛擬機器載入機制經典案例

package io.lgxkdream.test; class Father { static Father f = new Father(); static { System.out.println("father-1"); } { System.out.println("

jdk原始碼解析(七)——Java虛擬機器載入機制

前面我們講解了class檔案的格式,以及它是什麼樣的。那麼接下來需要了解它怎麼被載入到jvm中呢?jvm的載入機制又是怎麼一個過程呢?本文參考了《Java 虛擬機器規範(Java SE 7 版)》的第五章內容來詳細解釋一下 虛擬機器類載入機制:虛擬機器把描述類的資料從cla

Java虛擬機器載入時機

文章摘自:深入理解Java虛擬機器 第二版 周志明著  程式碼編譯的結果從本地機器碼轉變為位元組碼。 在Class檔案中描述的各種資訊,最終都需要載入到虛擬機器中之後才能執行和使用。虛擬機器把描述類的資料從class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以

Java 虛擬機器載入機制

看到這個題目,很多人會覺得我寫我的java程式碼,至於類,JVM愛怎麼載入就怎麼載入,博主有很長一段時間也是這麼認為的。隨著程式設計經驗的日積月累,越來越感覺到了解虛擬機器相關要領的重要性。閒話不多說,老規矩,先來一段程式碼吊吊胃口。public class SSClass{

Java虛擬機器載入機制

原文出處:http://www.importnew.com/18548.html類載入過程類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期包括:載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolut

深入理解Java虛擬機器 載入子系統2

與C/C++那些需要在編譯器期進行連線工作的語言不同,Java類的載入、連線和初始化都是在程式執行時完成的,只有在類被需要的時候才進行動態載入。 1)JVM何時載入類? 有且只有以下5種情況: 建立新物件(new)、設定/讀取static欄位(putst

Java虛擬機器載入器及雙親委派機制

所謂的類載入器(Class Loader)就是載入Java類到Java虛擬機器中的,前面《面試官,不要再問我“Java虛擬機器類載入機制”了》中已經介紹了具體載入class檔案的機制。本篇文章我們重點介紹載入器和雙親委派機制。 類載入器 在JVM中有三類ClassLoader構成:啟動類(或根類)載入器(Bo

深入理解Java虛擬機器-載入連線和初始化解析

不管學習什麼,我一直追求的是知其然,還要知其所以然,對真理的追求可以體現在方方面面。人生短短數十載,匆匆一世似煙雲,我認為,既然來了,就應該留下一些有意義的東西。本系列文章是結合張龍老師的《深入理解JVM》視訊做的一個筆記,其中將自己在學習過程中的實踐記錄、思考理解整合在了一起。希望在鞏固自己的知識時讓更多的

JVM(三)-java虛擬機器載入機制

概述:   上一篇文章,介紹了java虛擬機器的執行時區域,Java虛擬機器根據不同的分工,把記憶體劃分為各個不同的區域。在java程式中,最小的執行單元一般都是建立一個物件,然後呼叫物件的某個 方法。通過上一篇文章我們知道呼叫某個方法是通過虛擬機器棧的棧幀並通過執行引擎來實現的,但是實際上一個方法的執行前提

《深入理解Java虛擬機器》個人讀書總結——虛擬機器載入機制

我們都知道Java虛擬機器是用來執行我們編譯好的.class檔案的,class檔案中夾帶類的各種資訊,虛擬機器要執行這些檔案,第一件事就是要載入到虛擬機器中,這就引出了這次總結的問題——虛擬機器是如何載入這些class檔案的?載入後虛擬機器是怎麼處理檔案中夾帶的資訊的? 類載入機制

JAVA虛擬機器(七)虛擬機器載入機制

虛擬機器的類載入機制是指 把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗,轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別。類的載入連線和初始化過程都是在程式執行期間完成的。 類的生命週期: 載入->連線(驗證,準備,解析)->初始化->使用

深入理解Java虛擬機器筆記——虛擬機器載入機制

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

讀書筆記 ---- 《深入理解Java虛擬機器》---- 第6篇:虛擬機器載入機制

上一篇:類檔案結構:https://blog.csdn.net/pcwl1206/article/details/84197219 第6篇:虛擬機器類載入機制 1、概述 上一篇文章中講訴了Class檔案儲存格式的具體細節,在Class檔案中的描述的各種資訊,最終都要載入到虛擬機器中之後才

虛擬機器載入機制(七)——載入過程(初始化)

類初始化時類載入過程的最後一步,前面的類載入過程中,除了在載入階段(類載入過程的一個階段)應用程式可以通過自定義類載入器參與之外,其餘動作完全由虛擬機器主導和控制。到了初始化階段,才真正開始執行類中定義的java程式程式碼。 在準備階段,變數已經賦過一次系統要求的初始值,而