學習 JVM 的第 n-2 天,瞭解了類加載機制,以及初始化主動引用及被動引用的各類狀況,在此記錄分享。
Java虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的Java類型,這個過程被稱做虛擬機的類加載機制。java
上面這段話是在周志明大佬的《深刻理解Java虛擬機》中的,做爲類加載機制的概念在此摘錄。
當咱們在編譯器中選擇運行下面這個Hello World
程序,從點擊運行到程序中止運行會通過一系列複雜的過程,這些關於該類的過程就是類的生命週期。git
public class HelloWorld { public static void main(String[] args){ System.out.println("Hello World"); } }
類的生命週期分爲加載、驗證、準備、解析、初始化、使用、卸載七個階段。其中驗證、準備、解析三個階段被統稱爲鏈接。數組
下面,就針對各個階段作一個簡單的介紹。安全
在加載階段,虛擬機會經過類的全限定名查找並加載類的二進制字節流到方法區中。dom
咱們能夠經過在啓動時添加 JVM 參數-XX:+TraceClassLoading
,打開打印類的加載順序功能。函數
在驗證階段,虛擬機須要確保被加載類的正確性,符合虛擬機規範,以及不會危害虛擬機自身的安全。學習
在此階段中又有四個階段的驗證:文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證。具體可參考《深刻理解Java虛擬機(第三版)》7.3.2節。測試
在準備階段,虛擬機會爲類的靜態變量分配內存,並將其初始化爲默認值。spa
若是類字段的字段屬性表中存在ConstantValue
屬性的基本類型或字符串(即被final修飾),那在準備階段變量值就會被初始化爲初始值而非默認值。命令行
假設上文的HelloWorld
類中有兩個類變量定義以下,那麼在準備階段這兩個變量的值分別是 a=0,b=2。
public class HelloWorld { private static int a = 1; private static final int b = 2; }
在解析階段,虛擬機會將常量池內的符號引用替換爲直接引用。若是符號引用指向一個未被加載的類,那麼解析階段將觸發此類的加載。
在初始化階段,虛擬機會爲類的靜態變量賦予正確的初始值,這些賦值操做以及靜態代碼塊中的代碼會被編譯器統一置於一個<Clinit>
方法中,這個方法僅會被執行一次。因此咱們能夠根據類的靜態代碼塊是否執行來判斷一個類是否進行了初始化。
在 Java 虛擬機規範中規定了多種觸發初始化的狀況,被稱爲對類的主動引用。
在類的字節碼中遇到new、getstatic、putstatic、invokestatic
這四條指令時,會觸發類的初始化。
對於上述的觸發初始化的主動引用狀況有一些例外的狀況:
對於靜態字段,只有直接定義這個字段的類纔會被初始化,所以經過其子類來引用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。
經過數組定義來引用類,不會觸發此類的初始化。由於數組由 Java 虛擬機直接生成的,經過下面的例子來講明這一狀況。
在HelloWorld
類中定義一個主方法,並在主方法中定義兩個數組變量以下:
public class HelloWorld { public static void main(String[] args){ int[] a = new int[10]; MyObject[] b = new MyObject[10]; } } class MyObject { static { System.out.println("MyObject 類初始化..."); } }
而後對字節碼文件進行反編譯,在命令行中輸入javap -c HelloWorld
,獲得反編譯的字節碼指令以下:
➜ classes git:(master) ✗ javap -c HelloWorld Compiled from "HelloWorld.java" public class HelloWorld { public HelloWorld(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: bipush 10 2: newarray int 4: astore_1 5: bipush 10 7: anewarray #2 // class MyObject 10: astore_2 11: return }
能夠看到,對於原始類型的數組變量,在字節碼中經過指令newarray
完成建立;對於引用類型的數組變量,在字節碼中經過指令anewarray
完成建立。
而對於引用類型的MyObject
類,anewarray
這條指令與上文中敘述的幾種主動引用狀況不符合,不知足初始化的條件,這與上述測試代碼中沒有執行MyObject
類的靜態代碼塊的狀況相符,即沒有觸發MyObject
類的初始化。
當一個常量字段的值在編譯期不肯定時(如UUID.random().toString()
),那麼它不會被放到調用類的常量池中。所以即使這個靜態字段是一個常量(被final關鍵字修飾),但因爲它在編譯期是不肯定的,因此在程序運行時仍是會主動使用這個常量所在的類,從而觸發常量所在類的初始化。
一個接口在初始化時,並不要求其父接口所有都完成了初始化,只有在真正使用到父接口的時候(如引用接口中運行期才肯定的常量)纔會初始化。
因爲接口是沒法定義靜態代碼塊的,因此沒法像類那樣經過類靜態代碼塊的執行與否判斷是否發生初始化。可是在接口的初始化過程當中,編譯器一樣會爲接口生成<clinit>()
構造器,用於初始化接口中所定義的成員變量。
在初始化階段,虛擬機會爲類的靜態變量賦予正確的初始值,固然接口類也會聽從這條規定。因此咱們能夠經過初始化階段對靜態字段的賦值來觀察接口類是否進行了初始化,下面是驗證的過程。
在父類接口中定義一個int parentA = 1/0;
,而後經過子類訪問父類的parentRand
常量,根據上述第三條的結論,編譯期沒法肯定的常量parentRand
不會被放入調用類InterfaceDemo
的常量池,那麼必然會觸發子接口或父接口其中至少一個接口類的初始化(假設咱們不知道結論),若是觸發父接口的初始化,那麼會將1/0的值賦值給parentA
,當虛擬機計算1/0時,會拋出java.lang.ArithmeticException: / by zero
異常;若是隻觸發子接口的初始化,則不會拋出異常。
public class InterfaceDemo { public static void main(String[] args) { System.out.println(ChildInterface.parentRand); } } interface ParentInterface { int parentA = 1/0; String parentRand = UUID.randomUUID().toString(); } interface ChildInterface extends ParentInterface { String childRand = UUID.randomUUID().toString(); }
運行InterfaceDemo
類,輸出以下,其中InterfaceDemo.java:15
第15行正是parentA
定義的位置,從而能夠得出結論:在子接口使用到父接口時會觸發父接口的初始化。
Exception in thread "main" java.lang.ExceptionInInitializerError at InterfaceDemo.main(InterfaceDemo.java:10) Caused by: java.lang.ArithmeticException: / by zero at ParentInterface.<clinit>(InterfaceDemo.java:15) ... 1 more
而後將main
函數中的輸出改成ChildInterface.childRand
,運行的結果時輸出了一個UUID
,沒有拋出除零異常,從而得出結論:若子接口不使用父接口,不會觸發父接口的初始化。
綜上所述,驗證了第4條結論——一個接口在初始化時,並不要求其父接口所有都完成了初始化,只有在真正使用到父接口的時候(如引用接口中運行期才肯定的常量)纔會初始化。