JVM-類加載

類的生命週期:加載、驗證、準備、解析、初始化、使用和卸載這7個階段。其中,驗證、準備和解析可統稱爲鏈接。java

wKiom1h9qhnwLr_YAADkMj3URi8535.png

加載與鏈接階段的部份內容(如一部分字節碼文件格式驗證動做)是交叉進行的,加載階段還沒有完成,鏈接階段可能已經開始。可是這兩個階段總的開始時間和完成時間老是固定的,加載老是在鏈接以前開始,鏈接老是在加載完成以後完成。-Xverify:none關閉驗證,只有加載階段用戶可控,其它都由JVM完成。bootstrap

四個驗證階段:文件格式、元數據、字節碼、符號引用。數組

類加載過程的第一個階段:加載,此時虛擬機須要完成三件事情:緩存

       一、 經過類的全限定名來獲取類的二進制字節流。安全

           執行文件格式驗證,驗證字節流能正確地解析,驗證經過後,字節流存貯在方法區,後面的三個驗證都是基於方法區的存儲結構進行。數據結構

       二、 將字節流的靜態存儲結構轉化方法區的運行時數據結構。jvm

       三、 在堆中生成一個表明該類的java.lang.Class對象,做爲方法區這些數據的訪問入口。ide

準備階段:爲類的靜態變量分配內存並將其初始化爲默認值,若是字節碼含有ConstantValue屬性的字段(final 屬性),準備階段會將其初始化爲指定值,這些內存都將在方法區中進行分配。準備階段不分配類中的實例變量的內存,實例變量將會在對象實例化時隨着對象一塊兒分配在堆中。函數

類加載器
類加載由JVM外部實現,讓應用程序決定如何獲取所需的類,JVM提供了3種類加載器:
一、Bootstrap:根加載器,本地代碼(C++)實現,加載基礎核心類庫(rt.jar);
二、Extension:從java.ext.dirs系統屬性所指定的目錄中加載類庫,它的父加載器是Bootstrap;
三、System:應用類加載器,父類是Extension,是應用最普遍的類加載器。從classpath或者系統屬性java.class.path所指定的目錄中加載類,是用戶自定義加載器的默認父加載器。
優化

若使用自定義的類加載器(java.lang.ClassLoader的子類),則在字節碼的方法表存儲classLoader的引用,jvm在動態連接的時候,用該加載器加載引用類。爲了正確動態連接和維護多個命名空間,jvm須要知道方法表裏存貯的類加載器。

java.lang.ClassLoader內部維護着一個線程安全的HashTable<String,Class>,用於實現對Class字節流解碼後的緩存,若是HashTable中已經有了緩存,則直接返回緩存。
當class已經被Application類加載器加載過了,而後若是想要使用Extension類加載器加載這個類,將會拋出java.lang.ClassNotFoundException異常。

注意:父加載器不能查找子加載器裏的類。

類加載器能夠裝載一個類,卻不能夠卸載它,能夠刪除當前的類加載器,而後建立一個新的類加載器。


當一個類加載器被請求加載類時,在緩存裏查看這個類是否已經被本身裝載過了,若是沒有的話,繼續查找父類的緩存,直到在bootstrap類裝載器裏也沒有找到的話,它就會本身在文件系統裏去查找而且加載這個類。


類的預加載與首次主動使用
類加載器並不須要等到某個類被「首次主動使用」時再加載它。JVM規範容許類加載器在預料某個類將要被使用時就預先加載它,若是在預先加載的過程當中遇到了.class文件缺失或存在錯誤,類加載器必須在程序首次主動使用該類時才報告錯誤(LinkageError錯誤) 若是這個類一直沒有被程序主動使用,那麼類加載器就不會報告錯誤 。
類的加載會致使父類的類和接口也會被加載進來。
鏈接階段的符號引用的解析:

符號引用:
符號引用是一個字符串,它給出了被引用的內容的名字而且可能會包含一些其餘關於這個被引用項的信息——這些信息必須足以惟一的識別一個類、字段、方法。這樣,對於其餘類的符號引用必須給出類的全名。對於其餘類的字段,必須給出類名、字段名以及字段描述符。對於其餘類的方法的引用必須給出類名、方法名以及方法的描述符。

直接引用

直接引用解析後,放到運行時常量池裏。

一、對於類的Class對象、類變量、類方法的直接引用多是指向方法區的本地指針。

二、對於實例變量、實例方法的直接引用都是偏移量。

實例變量的直接引用多是從對象的映像開始算起到這個實例變量位置的偏移量。

實例方法的直接引用多是方法表的偏移量。

子類中方法表的偏移量和父類中的方法表的偏移量是一致的,好比說父類中有一個say()方法的偏移量是7,那麼子類中say方法的偏移量也是7。
經過「接口引用」來調用一個方法,jvm必須搜索對象的類的方法表才能找到一個合適的方法。這是由於實現同一個接口的這些類中,不必定全部的接口中的方法在類方法區中的偏移量都是同樣的。他們有可能會不同。這樣的話可能就要搜索方法表才能確認要調用的方法在哪裏。
而經過「類引用」來調用一個方法的時候,直接經過偏移量就能夠找到要調用的方法的位置了。【由於子類中的方法的偏移量跟父類中的偏移量是一致的】
因此,經過接口引用調用方法會比類引用慢一些。

初始化:

若是碰到在本類中聲明本類的靜態對象,且實例化,<cinit>()嵌套<init>()方法,則實例初始化可能在類初始化以前。

到了初始化階段,才真正開始執行類中定義的Java程序代碼。

一、static final int VAL = 100,編譯時肯定的常量:基本數據類型的常量、String,不包括任何new對象和須要在運行時才能肯定的值。編譯階段會爲VAL生成ConstantValue屬性,在準備階段虛擬機會根據ConstantValue屬性將VAL賦值爲100。

基本數據類型(不含包裝類)的常量拷貝,不進入常量池,編譯器把他們看成值(value)而不是域(field)來對待。直接把這個值插入到字節碼中。這是一種頗有用的優化,若是是byte、short、int 數據,還會根據實際精度選擇不一樣類型的字節碼命令,如bipush、sipush、iconst,和定義的類型不要緊。long類型是ldc命令。String的ldc #常量池編號。

二、類初始化:執行<clinit>()方法,是由javac自動收集類中的全部類變量的賦值動做和靜態語句塊static{}中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,如:

 static int i=1; static{i=0;}  //i最終是0

三、接口的<clinit>()方法不須要先執行父接口的<clinit>()方法。只有當父接口中定義的變量使用時,父接口才會初始化。

     接口的實現類在初始化時也同樣不會執行接口的<clinit>()方法。 

四、虛擬機規範嚴格規定了有且只有5中狀況(jdk1.7)若是類沒有進行過初始化,必須對類進行「初始化」:

 1.  new:建立對象(經過數組定義來引用類,不觸發初始化)。

    getstatic、putstatic讀取/設置靜態非final變量,如:static int a = 1,準備階段賦初始值0,初始化階段賦定義值1,誰定義初始化誰,和調用者無關。

    invokestatic:執行靜態方法。

 2.使用java.lang.reflect包的方法對類進行反射調用。

 3.子類初始化,觸發父類的初始化,虛擬機會保證父類的<clinit>優先執行,則父類中定義的靜態語句塊要優先於子類的變量賦值操做。

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

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

五、如下狀況不會觸發初始化:

定義對象數組,不會觸發該類的初始化

經過類名獲取Class對象,不會觸發類的初始化。

經過Class.forName加載指定類時,若是指定參數initialize爲false時,也不會觸發類初始化

經過ClassLoader默認的loadClass方法,也不會觸發初始化動做,new ClassLoader(){}.loadClass("xxx.Cat");

對象建立過程:

1在堆內存中開闢一塊空間,並給空間分配一個地址

2把對象的全部非靜態成員加載到所開闢的空間下,並進行默認初始化,而後調用構造函數。

在構造函數入棧執行時,分爲兩部分:先執行構造函數中的隱式三步,再執行構造函數中書寫的代碼

  6.一、隱式三步:

      1,執行super語句

      2,對開闢空間下的全部非靜態成員變量進行顯式初始化

      3,執行構造代碼塊

  6.二、在隱式三步執行完以後,執行構造函數中書寫的代碼

7在整個構造函數執行完並彈棧後,把空間分配的地址賦值給一個引用對象

相關文章
相關標籤/搜索