JVM-類加載

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

wKiom1h9qhnwLr_YAADkMj3URi8535.png

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

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

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

       1、 通過類的全限定名來獲取類的二進制字節流。

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

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

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

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

類加載器
類加載由JVM外部實現,讓應用程序決定如何獲取所需的類,JVM提供了3種類加載器:
1、Bootstrap:根加載器,本地代碼(C++)實現,加載基礎核心類庫(rt.jar);
2、Extension:從java.ext.dirs系統屬性所指定的目錄中加載類庫,它的父加載器是Bootstrap;
3、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錯誤) 如果這個類一直沒有被程序主動使用,那麼類加載器就不會報告錯誤 。
類的加載會導致父類的類和接口也會被加載進來。
連接階段的符號引用的解析:

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

直接引用

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

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

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

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

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

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

初始化:

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

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

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

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

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

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

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

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

4、虛擬機規範嚴格規定了有且只有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的方法句柄,並且這個方法句柄所對應的類沒有進行初始化,則需要先出觸發其初始化。

5、以下情況不會觸發初始化:

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

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

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

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

對象創建過程:

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

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

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

  6.1、隱式三步:

      1,執行super語句

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

      3,執行構造代碼塊

  6.2、在隱式三步執行完之後,執行構造函數中書寫的代碼

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


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章