Java類加載的過程

JVM和類
當咱們調用 Java 命令運行某個 Java 程序時,該命令將會啓動一條 Java 虛擬機進程,無論該 Java 程序有多麼複雜,該程序啓動了多少個線程,它們都處於該 Java 虛擬機進程裏。同一個 JVM 的全部線程、全部變量都處於同一個進程裏,它們都使用該 JVM 進程的內存區。當系統出現如下幾種狀況時, JVM 進程將被終止:java

程序運行到最後正常接收;
程序運行到使用System.exit()或Runtime.getRuntime().exit()代碼結束程序;
程序運行中遇到未捕獲的異常或錯誤結束;
程序所在平臺強制結束了JVM進程;
類加載器就是尋找類或接口字節碼文件進行解析並構造JVM內部對象表示的組件,在java中類裝載器把一個類裝入JVM,通過如下步驟:數據庫

一、加載:查找和導入Class文件編程

二、連接:其中解析步驟是能夠選擇的 (a)檢查:檢查載入的class文件數據的正確性 (b)準備:給類的靜態變量分配存儲空間 (c)解析:將符號引用轉成直接引用數組

三、初始化:對靜態變量,靜態代碼塊執行初始化工做安全

類的加載過程
當Java程序須要使用某個類時,若是該類還未被加載到內存中,JVM會經過加載、鏈接(驗證、準備和解析)、初始化三個步驟來對該類進行初始化。網絡

類的加載是指把類的.class文件中的數據讀入到內存中,一般是建立一個字節數組讀入.class文件,而後產生與所加載類對應的Class對象。加載完成後,Class對象還不完整,因此此時的類還不可用。當類被加載後就進入鏈接階段,這一階段包括驗證、準備(爲靜態變量分配內存並設置默認的初始值)和解析(將符號引用替換爲直接引用)三個步驟。最後JVM對類進行初始化,包括:數據結構

1)若是類存在直接的父類而且這個類尚未被初始化,那麼就先初始化父類;多線程

2)若是類中存在初始化語句,就依次執行這些初始化語句。函數

概述
因爲Java的跨平臺性,通過編譯的Java源程序並非一個可執行程序,而是一個或多個類文件。當Java程序須要使用某個類時,若是該類還未被加載到內存中,Java虛擬機會經過加載、鏈接和初始化一個Java類, 使該類能夠被正在運行的Java程序所使用。其中,加載(Loading)就是把類的.class文件讀入Java虛擬機中; 而鏈接(Linking)就是把這種已經讀入虛擬機的二進制形式的類型數據合併到虛擬機的運行時狀態中去 。鏈接階段分爲三個子步驟——驗證(Verification)、準備(Preparation)和解析(Resolution)。 驗證步驟確保了Java類型數據格式正確而且適於Java虛擬機使用。而準備步驟則負責爲該類型分配它所需的內存、好比爲它的類變量分配內存。解析步驟則負責把常量池中的符號引用轉換爲直接引用。佈局

加載、 驗證、準備和初始化這四個階段的順序是肯定的,類的加載過程必須按照這種順序接部就班地開始,而解析則不必定: 它在某些狀況下能夠在初始化階段以後再開始, 這是爲了支持 Java語言的運行時綁定 (也稱爲動態綁定或晩期綁定)。

類初始化的時機
在類和接口被加載和鏈接的時機上, Java虛擬機規範給實現提供了必定的靈活性 。可是它嚴格地定義了初始化的時機 。全部的Java虛擬機實現必須在每一個類或接口首次主動使用時初始化 。下面這幾種情形必須當即對類進行「初始化」:

1)遇到 new、 getstatic、 putstatic或invokestatic這4條字節碼指令時,若是類沒有進行過初始化, 則須要先觸發其初始化, 生成這4條指令的最多見的 Java代碼場景是:

使用 new關鍵字實例化對象的時候
讀取或設置一個類的靜態字段的時候(即在字節碼中,執行getstalic或putstatic指令時),被final修飾、已在編譯期把結果放入常量池的靜態字段除外
調用一個類的靜態方法的時候(即在字節碼中執行invokestatic指令時)。
2 ) 當調用Java API中的某些反射方法時, 好比類Class中的方法或者java.lang.reflect包的方法對類進行反射調用的時候, 若是類沒有進行過初始化 , 則須要先觸發其初始化。

3 ) 當初始化一個類的時候, 若是發現其父類尚未進行過初始化, 則須要先觸發其父類的初始化。

  1. 當虛擬機啓動時, 用戶須要指定一個要執行的主類(包合 main()方法的那個類) . 虛擬機會先初始化這個主類。

5)當使用JDK1.7的動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化。

對於這五種會觸發類進行初始化的場景, 虛擬機規範中使用了一個很強烈的限定語:「有且只有 '', 這5種場景中的行爲稱爲對一個類進行主動引用 。 除此以外,全部引用類的方式都不會觸發初始化, 稱爲被動引用。

類的加載
加載是類加載過程的一個階段,這兩個概念必定不要混淆。在加載階段, 虛擬機須要完成如下三件事情:

1)經過一個類的全限定名來獲取定義此類的二進制字節流。

2 )將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。

3 ) 將類的class文件讀入內存,併爲之建立一個java.lang.Class對象,也就是說當程序中使用任何類時,系統都會爲之創建一個java.lang.Class對象, 做爲方法區這個類的各類數據的訪問入口。

經過使用不一樣的類加載器,能夠從不一樣來源加載類的二進制數據,一般有以下幾種來源:

從本地文件系統加載class文件;
從一個ZIP、 JAR、 CAB或者其餘某種歸檔文件中提取Java class文件,JDBC編程時使用到的數據庫驅動就是放在JAR文件中,JVM能夠直接從JAR包中加載class文件;
經過網絡加載class文件,這種場景最典型的應用就是 Applet;
把一個java源文件動態編譯、並執行加載
運行時計算生成, 這種場景使用得最多的就是動態代理接術, 在 java.lang.reflect.Proxy中 , 就是用了 ProxyGenerator.generateProxyClass來爲特定接口生成形式爲「*$Proxy」的代理類的二進制字節流。
類的鏈接
當類被加載後,系統爲之生成一個對應的Class對象,接着會進入鏈接階段,鏈接階段將會負責把類的二進制文件合併到JRE中。類鏈接分爲以下三個階段:

驗證:驗證階段用於檢驗被加載的類是否有正確的內部結構,並和其餘類協調一致;
準備:準備階段則負責爲類的靜態屬性分配內存,並設置默認初始值;
解析:將類的二進制數據中的符號引用替換成直接引用(符號引用是用一組符號描述所引用的目標;直接引用是指向目標的指針)
驗證
驗證是鏈接階段的第一步, 這一階段的目的是爲了確保 Class文件的字節流中包含的信息符合當前虛擬機的要求, 井且不會危害虛擬機自身的安全。

Java語言自己是相對安全的語言,但前面已經說過, Class文件並不必定要求用 Java源碼編譯而來, 可使用任何途徑, 包括用十六進制編譯器直接編寫來產生 Class 文件。在字節碼的語言層面上, 上述 Java代碼沒法作到的事情都是能夠實現的, 至少語義上是能夠表達出來的。虛擬機若是不檢査輸入的字節流,對其徹底信任的話, 極可能會由於載入了有害的字節流而致使系統崩潰 , 因此驗證是虛擬機對自身保護的一項重要工做。從總體上看,驗證階段會完成下面四個階段的檢驗過程: 文件格式驗證、 元數據驗證、 字節碼驗證、符號引用驗證。

一、文件格式驗證

第一階段要驗證字節流是否符合 Class文件格式的規範, 井且能被當前版本的虛擬機處理。這一階段可能包括下面這些驗證點:

是否以魔數 0xCAFEBABE開頭
主、次版本號是否在當前虛擬機處理範圍以內 。
常量池的常量中是否有不被支持的常量類型(檢査常量tag 標誌)。
指向常量的各類索引值中是否有指向不存在的常量或不符合裝型的常量 。
CONSTANT_Utf8_info型的常量中是否有不符合 UTF8編碼的數據
Class 文件中各個部分及文件自己是否有被刪除的或附加的其餘信息
實際上第一階段的驗證點還遠不止這些, 上面這些只是從 HotSpot虛擬機源碼中摘抄的一小部分而已。只有經過了這個階段的驗證以後, 字節流纔會進入內存的方法區中進行存儲, 因此後面的三個驗證階段所有是基於方法區的存儲結構進行的,不會再直接操做字節流。

二、元數據驗證

第二階段是對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求,這個階段可能包括的驗證點以下:

這個類是否有父類(除了 java.lang.0bject以外,全部的類都應當有父類)

這個類的父類是否繼承了不容許被繼承的類(被finaI修飾的類)

若是這個類不是抽象類, 是否實現了其父類或接口之中要求實現的全部方法

類中的字段、 方法是否與父類產生了矛盾(例如覆蓋了父類的final字段, 或者出現不符合規則的方法重載, 例如方法參數都一致, 但返回值類型卻不一樣等)

第二階段的驗證點一樣遠不止這些,這一階段的主要目的是對類的元數據信息進行語義檢驗, 保證不存在不符合 Java語言規範的元數據信息。

三、字節碼驗證

第三階段是整個驗證過程當中最複雜的一個階段, 主要目的是經過數據流和控制流的分析,肯定語義是合法的。符號邏輯的。在第二階段對元數據信息中的數據類型作完校驗後,這階段將對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會作出危害虛擬機安全的行爲,例如:

保證任意時刻操做數棧的數據裝型與指令代碼序列都能配合工做, 例如不會出現相似這樣的狀況:在操做棧中放置了一個 int類型的數據, 使用時卻按long類型來加載入本地變量表中。
保證跳轉指令不會跳轉到方法體之外的字節碼指令上
保證方法體中的類型轉換是有效的, 例如能夠把一個子類對象賦值給父類數據裝型,這是安全的,可是把父類對象意賦值給子類數據類型,甚至把對象賦值給與它毫無繼承關係、 徹底不相干的一個數據類型, 則是危險和不合法的。
即便一個方法體經過了字節碼驗證, 也不能說明其必定就是安全的。

四、符號引用驗證

最後一個階段的校驗發生在虛擬機將符號引用轉化爲直接引用的時候 , 這個轉化動做將在鏈接的第三個階段——解析階段中發生。符號引用驗證能夠看作是對類自身之外(常量池中的各類符號引用) 的信息進行匹配性的校驗, 一般須要校驗如下內容:

符號引用中經過字將串描述的全限定名是否能找到對應的類
在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段 。
符號引用中的類、字段和方法的訪問性(private、 protected、 public、 default)是否可被當前類訪問
符號引用驗證的目的是確保解析動做能正常執行, 若是沒法經過符號引用驗證, 將會拋出一個 java.lang.IncompatibleClassChangError異常的子類, 如 java.lang.IllegalAccessError、 java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。

對於虛擬機的裝加載機制來講 ,驗證階段是一個很是重要的、 但不必定是必要的階段(由於對程序沒有影響)。若是所運行的所有代碼(包括本身編寫的以及第三方包中的代碼)都已經被反覆使用和驗證過 , 那麼在實施階段就能夠考慮使用一Xverify;none 參數來關閉大部分的驗證措施, 以縮短虛擬機類加載的時間。

準備
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配 。這個階段中有兩個容易產生混淆的概念須要強調一下, 首先,這時候進行內存分配的僅包括類變量(被static修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒分配在 Java 堆中 。 其次,這裏所說的初始值「一般狀況」下是數據類型的零值。

解析
解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程, 解新動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行,分別對應於常量池的CONSTANT_Class_info、 CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_IntrfaceMethodref_info、CONSTANT_MethodType_info、CONSTANT_MethodHandle_info和CONSTANT_InvokeDynamic_info7種常量類型,解析階段中所說的直接引用與符號引用關係以下:

符號引用(Symlxiuc References):符號引用以一組符號來描述所引用的日標,符號能夠是任何形式的字面量, 只要使用時能無歧義地定位到目標便可, 特號引用與配組機實現的內存1布.局11i-美 , 引用的日標並不必定已組加裁到內存中
直接引用(Direct References):直接引用能夠是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現的內存佈局相關的 , 同一個符號引用在不一樣虛擬機實例上翻譯出來的直接引用通常不會相同. 若是有了直接引用, 那引用的目標一定已經在內存中存在
類的初始化
初始化階段是類加載過程的最後一步 , 前面的幾個階段, 除了在加載階段用戶應用程序能夠經過自定 義類加載器參與以外, 其他動做徹底由虛擬機主導和控制。到了初始化階段, 才真正開始執行類中定義的 Java程序代碼。從代碼角度,初始化階段是執行類構造器 ()方法的過程。咱們先看一下 ()方法執行過程當中可能會影響程序運行行爲的特色和細節:

()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜志語句塊(static{}塊)中的語句合併產生的, 編譯器收集的順序是由語句在源文件中出現的順序所決定的, 靜態語句塊中只能訪問到定義在靜態語句塊以前的變量, 定義在它以後的変量 , 在前面的靜態語句塊能夠賦值 , 可是不能訪問
()方法與類的構造函數 (或者說實例構造器 ()方法)不一樣,它不須要顯式地調用父類構造器, 虛期機會保證在子類的 ()方法執行以前, 父類的 ()方法已經執行完畢, 所以在虛期機中第一個被執行的 ()方法的類確定是 java,lang.Object
因爲父類的 ()方法先執行,也就意味着父類中定義的靜態語句塊要優先於子類的變量賦值操做
()方法對於類或接口來講並非必須的, 若是一個類中沒有靜態語句塊,也沒有對變量的賦值操做, 那麼編譯器能夠不爲這個類生成 ()方法
接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操做, 所以接口與類同樣都會生成 ()方法。 但接口與類不一樣的是, 執行接口的 ()方法不須要先執行父接口的 ()方法。只有當父接口中定義的變量被使用時, 父接口才會被初始化。 另外, 接口的實現類在初始化時也同樣不會執行接口的 ()方法
虛擬機會保證一個類的 ()方法在多線程環境中被正確地加鎖和同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的 ()法,其餘線程部須要阻塞等待,直到活動線程執行 ()方法完畢。若是在一個類的 ()方法中有耗時很長的操做, 那就可能形成多個進程阻塞, 在實際應用中這種阻塞每每是隱蔽的。
類的初始化階段主要是對類變量進行初始化,在Java類中對類變量指定初始值有兩種方式:

聲明類變量時指定初始值
使用靜態初始化塊爲類變量指定初始值
JVM初始化一個類通常包括以下幾個步驟:

假如這個類尚未被加載和鏈接,程序先加載並鏈接該類;
假如該類的直接父類尚未被初始化,則先初始化其直接父類;
假如類中有初始化語句,則系統依次執行這些初始化語句
當執行第二步時,系統對直接父類的初始化也遵循此一、二、3步驟,若是該直接父類又有直接父類,系統再次重複這三步,因此JVM最早初始化的老是java.lang.Object類。

參考:

http://www.jb51.net/article/112006.htm

http://www.javashuo.com/article/p-rtbvcfpv-ey.html

相關文章
相關標籤/搜索