java虛擬機學習(四)類的加載過程

  類從虛擬機內存加載到從內存卸載,經歷的生命週期是:加載,驗證,準備,解析,初始化,使用,卸載這幾個階段, 其中驗證,解析,初始化被稱爲 鏈接過程(Linking).java

(打算這塊和類加載原理後再看class文件結構那篇)   
數組

  除了解析和使用,其餘的過程基本順序就是這樣, 解析能夠是在初始化完成以後,這是爲了運行時動態綁定。安全

   在虛擬機規範中定義了5中狀況(有且只有)必須對類進行初始化(以前進行過,加載,驗證,準備):
網絡

    1.碰到new,getstatic,putstatic,invokestatic這4條字節碼指令時,若是類沒有進行初始化時,
數據結構

    2.在使用反射時,若是類沒有初始化。
多線程

    3.在這個類的父類沒有初始化時。
jsp

    4.虛擬機啓動時指定的Main類,即主類。
佈局

    5.使用動態語言支持時,特定的方法句柄對應的類沒有初始化時(REF_getStatic,REF_putStatic,REF_invokeStatic)
spa


類的加載線程

    1.獲取定義一個類的二進制流,二進制流能夠是從網絡獲取,zip包,jar包均可以,也出如今jsp,動態代理(java.lang.reflect.Proxy)等。

    2.將二進制流表明的靜態存儲結構轉化爲方法區運行時數據結構。

    3.內存中生成表明這個類的java.lang.class對象,做爲方法區這個類的各類數據訪問入口。

非數組類是開發者可控性最強的,它能夠有系統提供的classLoader加載,也能夠有用戶自定義的classLoader加載,而數組類是由虛擬機直接建立,但數組類的類元仍是由classLoader來加載。數組類的建立過程遵循一下原則:

    1.若是數組的組件類型(相似與 Foo[] fooArray)爲引用類型,數組將在該組件類型的類加載器的類名稱空間上被標識。   

    2.若是數組的組件類型不是引用類型(如 int[]),java虛擬機將會把數組標記爲與引導類加載器關聯。

    3.數組類的可見性與它的組件類型的可見性一致

加載完成後,在內存(並無明確規定是在java堆中,class類對象比較特殊,在HotSpot虛擬機中這塊內存指的是方法區內存)中實例化類的java.lang.class對象,這個對象做爲程序訪問方法區數據類型的外部接口,加載與鏈接過程是交叉進行的(由於鏈接過程當中包含驗證過程),其餘鏈接過程與加載過程依然保持順序執行。

驗證:

    java 的class文件並不必定是由java源碼生成,它能夠由十六進制編譯器直接編寫來產生,但java虛擬機不會去訪問數組邊界之外的數據,也不會將對象轉換爲沒有實現的類,執行不存在的代碼之類的事情。但這些都須要通過虛擬機的驗證過程。也是防止虛擬機遭受惡意代碼的攻擊,若是要驗證的流沒有通過class文件格式的規範則拋出java.lang.VerifyError或其子類異常。詳細的參考java虛擬機規範 . 大體的驗證有如下4個階段檢驗動做:

    1.文件格式驗證:    

        這個階段驗證字節碼是否符合規範及該虛擬是否能處理,包括一下驗證點:

        是否以魔數0xCAFEBABEK開頭。

        主,次版本是否在當前虛擬機處理範圍內。

        常量池是否含有不被支持的常量類型等。

      遠不止這些,不過這個驗證經過後字節碼才能進入內存的方法區並轉換爲運行時的數據結構。

    2.元數據驗證:

       主要是檢測元數據是否符合java規範如:

        這個類是否有父類。

        類是否繼承了不容許繼承的類。

        是否實現了接口的全部方法等。


    3.字節碼驗證(應該能夠理解爲方法的合法性驗證):

        整個驗證過程當中最複雜的一步,主要是經過數據流和控制流分析,程序語法的合法性,確保符合邏輯的,在第二階段對元數據驗證結束後,這個階段主要是對類的方法體進行驗證,保證運行時不會危害虛擬機 ,如:

        1.保證操做數棧的數據類型與指令代碼序列配合工做且不會出現棧中是int類型,使用時卻以long的方式載入到本地變量表,    

        2.保證跳轉指令不會跳轉到方法體之外的字節碼指令上。

        3.保證類型轉換是有效的。好比把子類對象賦值給父類對象這是安全的,但把父類對象賦值給子類的,甚至把對象賦值給與之不相干的類,前者是危險的後者是不合法的。

        但這個過程以後也並不能徹底保證它是安全的。 (關鍵字:StackMapTable, -XX:-UseSpliteVerifier, -XX:+FailOverToOldVerifier 之後用到的時候在看。先記下來:) )

    4.符號引用驗證(類,方法,字段是否可解析的驗證):

        目的是確保解析動做能正常運行,若是沒法經過符號驗證會拋出,java.lang.incompatibleClassChangeError的子類,如:java.lang.IllegalAccessError,java.lang.NoSuchFieldError,java.lang.NoSuchmethodError等,若是所運行的代碼已經被反覆使用和驗證過,能夠考慮使用-Xverify:none參數關閉大部分類驗證,以縮短類加載時間。

        一般須要校驗的內容:    

        1.符號引用中經過字符串描述的全限定名是否能找到對應的類。

        2.在指定的類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法字段(應該是可否根據字段描述找到方法這個意思)。

        3.符號引用中的類,字段,方法是否可被當前類訪問(修飾符修飾的訪問可能行)。

準備:

    準備階段正式爲類的變量分配內存,這裏的內存指的是方法區,變量是靜態變量,非靜態變量和類對象實例都是在java堆中分配內存,如:

    

static int value=123

在準備階段value的值爲0,這個時候方法尚未執行,只有經歷初始化階段value的值纔會是123(putstatic是編譯後存放於類構造器<clinit>()方法中),基本數據類型爲 數字的都是0, 0f,0l,0d等,char爲\u0000,String 不是基本類型,reference是null, 若是以上int 被final修飾過,則準備階段虛擬機會根據ConstantValue的設置將value設置爲123      


解析:

    解析階段是把符號引用轉換爲直接引用的過程。 

    符號引用:

    以一組符號來描述所引用目標(java類並不知道引用類的實際內存地址,所以只能使用符號引用來代替),與虛擬機的內存佈局無關,引用的目標不必定已經加載到內存,各類虛擬機實現的內存佈局能夠不相同,可是他們能接受的符號引用必須相同,符號引用的字面量形式明肯定義在java虛擬機規範class文件中。

    直接引用:

    直接引用能夠是指向目標的指針,偏移量,或是能間接定位的目標句柄。與內存佈局相關,能直接引用說明對象已經存在與內存中,不一樣的虛擬機實例上翻譯出來的直接引用可能不同。

虛擬機根據須要判斷,是在類加載時對常量池中符號作解析 ,解析字段,方法,接口的過程當中會去class_index中索引所屬的類 ,並解析類符號,在此過程當中若是出錯,都會致使以上解析沒法繼續進行。

  1.類或接口解析:

    若是代碼所處的類爲A, 要將爲解析過的符號引用N解析爲一個類或接口C的直接引用(也就是類A 裏有類C的引用)。須要一下步驟。

        1.若是這個類C不是數組類型,那虛擬機會把符號引用的全限名傳遞給類D加載器去加載,可能會觸發其餘相關類的加載動做,如:該類的父類或實現接口。若是加載過程當中出現任何異常,解析會失敗。

        2.若是C是數組,且元素爲對象類型,則會去加載該對象,以後虛擬機生成一個表明此數組緯度和元素的數組對象。

        3.上面的步驟完成後要進行符號引用驗證,肯定D是否有對C的訪問全限,若是不具備則拋出java.lang.illgalAcessError。

    2.字段解析:

    解析一個爲被解析過的字段引用,首先會對字段表內class_index中索引的CONSTANT_CLASS_info符號引用作解析,字段所屬的類或接口符號引用的過程出現異常,會致使字段符號引用的失敗。若成功則有如下步驟:

        1.若是一個類自己包含了簡單名稱和字段描述都與目標字段匹配,則返回這個字段引用,查找結束。

        2.不然去查找C是否實現了接口,若是是會往上遞歸查詢,若是存在名稱和字段描述都匹配,返回引用查找結束。

        3.不然去查找父類當中是否存在,若是存在名稱和字段描述都匹配,返回引用查找結束。

        4.若是父類,接口都沒有查到,拋出java.lang.NoSuchFieldError異常。

    若找到了可是沒有訪問全限,則拋出java.lang.IllegalAccessError.實際上可能會更嚴格,若字段在父類和接口中屢次出現,可能會拒絕編譯。

    3.方法解析;

        方法解析中, 類方法解析和接口方法解析是有區別的,在常量池中這倆符號引用不一樣。

          1.一旦一個類被解析爲類而非接口,在類方法表中若是這個方法所屬的類是接口,那麼直接會拋出java.lang.incompatibleClassChangeError,換句話說是嘗試引用一個純接口而非實現類的方法時,會拋出此異常。

          2.若是簡單名稱與描述符號都與目標匹配的方法,則返回這個方法的引用,查找結束。

          3.若是第二部沒有找到會往上遞歸去父類中尋找。

          4.若是父類中未找到,則會去實現的接口或接口的父類中去查找,若是找到則會拋出,java.lang.AbstractMethodError,說明該類爲抽象類。

          5.若是以上都沒有找到,則會拋出java.lang.NoSuchMethodError。

       查找成功會去作權限驗證,若是無訪問全限則報:java.lang.IllegalAccessError異常

    4:接口解析:

        1.第一條與方法解析相反,查找是在接口方發表中查詢, 若是這個方法包含的接口是類,則會報java.lang.incompatibleClassChangeError。

        2.以後去判斷簡單名稱和描述符號都與目標匹配,若是查到了就返回這個方法的直接引用。

        3.不然一直會遞歸到object類,若是找到則返回直接引用,若是沒找到則報java.lang.NoSuchMethodError異常。

    在成功的狀況下跟類方法解析成功同樣都會去作全限檢查

 初始化:

    首先會執行<cinit>方法,cinit方法並非必定產生 只有當類中有static和變量初始賦值時產生的,在子方法執行<cinit>以前保證執行完父類的<cinit>方法,接口中雖然沒有靜態變量塊可是,仍然會產生<cinit>方法,接口執行cinit方法前不須要執行父類的cinit方法,接口的實現類初始化也不會執行接口的cinit方法,在多線程環境中,一個類的cinit執行會阻塞其餘類執行這個類的cinit方法,並且只執行一次。

    

 到此類的加載過程結束,這個時候的類才能真正去使用。

相關文章
相關標籤/搜索