類的生命周期:加載、連接(驗證、准備、解析)、初始化、使用、卸載
主動引用(有且只有)初始化:
1.new、getstatic、putstatic、invokestatic如果類沒初始化,則初始化new關鍵字實例化對象、讀取或設置一個類的靜態字段(被final修飾、*已在編譯期把結果放入常量池的靜態字段除外)、調用一個類的靜態方法
2.使用java.lang.reflect包的方法對類進行發射調用的時候,如果類沒有進行過初始化,則初始化
3.當初始化一個類的時候,父類沒初始化,則初始化
4.當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main方法的那個類),虛擬機會先初始化這個主類
static final int a = 1;//是一個編譯時常量
static final int b = "test".length();//是一個運行時常量
被動引用:
1.通過子類引用父類的靜態字段,不會導致子類初始化
2.通過數組定義來引用類,不會觸發此類的初始化
3.常量在編譯階段會存入調用類的常量池中,本質上沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化
父類代碼:
1 public Class Superclass{ 2 public static int values = 123; 3 public static final String hello = "HELLO"; 4 static{ 5 System.out.println("Superclass init"); 6 } 7 }
子類繼承Superclass:
1 public Class Subclass extends Superclass{ 2 static{ 3 System.out.println("Subclass init"); 4 } 5 }
測試類:
1 public Class Test{ 2 public static void main(String[] args){ 3 System.out.println(Subclass.values);//通過子類引用父類的靜態字段 4 Superclass sca = new Superlass[10];//通過數組定義來引用類,不會觸發此類的初始化 5 System.out.println(Superclass.hello);//使用常量 6 } 7 }
下面分解介紹Java類生命周期的五個階段:
類加載階段:
1.通過一個類的全限定名來獲取定義此類的二進制字節流
2.將這個字節流代表的靜態儲存結構轉化為方法區的運行時數據結構**
3.在Java堆中生成一個代表這個類的java.lang.class對象,作為方法區這些數據的訪問入口**
加載階段完成后,虛擬機外部的二進制字節流就按照jvm所需的格式存儲在方法區中,方法區的數據儲存格式由虛擬機實現自行定義,jvm規范未規定此區域的具體數據結構。然后再Java堆中實例化一個java.lang.class類的對象,這個對象將作為程序訪問方法區中的這些類型數據的外部接口。加載階段與連接階段的部分內容(如一部分字節碼文件格式驗證動作)是交叉進行的,加載階段尚未完成連接階段可能已經開始,但這些夾在加載階段之中進行的動作,讓然屬於連接階段的內容,這連個階段開始時間仍然保持着固定的先后順序。
驗證階段:連接階段的第一步,這階段是為了確保class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害自身的安全。
1.文件格式驗證
2.元數據驗證
3.字節碼驗證
4.符號引用驗證
准備階段:准備階段是正式為變量分配內存並設置類變量初始化的階段,這些內存都將在方法區中進行分配。此時內存分配的變量僅包括類變量(static修飾的),而不包括實例變量,實例變量將在對象實例化時隨着對象一起分配在Java堆中。初始值通常是數據類型的零值.****
特殊情況Constantvalue屬性,會被賦值為Constantvalue屬性的值如public static final int value = 123;
解析階段:jvm將常量池內的符號引用替換為直接引用的過程
1.符號引用(Symbolic References):符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能夠無歧義的定位到目標即可。例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等類型的常量出現。符號引用與虛擬機的內存布局無關,引用的目標並不一定加載到內存中。在Java中,一個java類將會編譯成一個class文件。在編譯時,java類並不知道所引用的類的實際地址,因此只能使用符號引用來代替。比org.simple.People類引用了org.simple.Language類,在編譯時People類並不知道Language類的實際內存地址,因此只能使用符號org.simple.Language(假設是這個,當然實際中是由類似於CONSTANT_Class_info的常量來表示的)來表示Language類的地址。各種虛擬機實現的內存布局可能有所不同,但是它們能接受的符號引用都是一致的,因為符號引用的字面量形式明確定義在Java虛擬機規范的Class文件格式中。
2.直接引用:
(1)直接指向目標的指針(比如,指向“類型”【Class對象】、類變量、類方法的直接引用可能是指向方法區的指針)
(2)相對偏移量(比如,指向實例變量、實例方法的直接引用都是偏移量)
(3)一個能間接定位到目標的句柄
直接引用是和虛擬機的布局相關的,同一個符號引用在不同的虛擬機實例上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目標必定已經被加載入內存中了。***
初始化階段:類的初始化方法:<clinit>()
刷新准備階段的初始值,根據程序主觀計划去初始化類變量和其他資源。初始化階段是執行構造器<clinit>()方法的過程。<clinit>()方法是由編譯器自動收集類中所有類變量的賦值動作和靜態語句塊(static{}塊)中的語句合並產生的,編譯期收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量,定義在他之后的變量,在前面的靜態語句塊中可以賦值,但是不能訪問。
<clinit>()方法與類的構造函數(或者說實例構造器<init>()方法)不同,它不需要顯式地調用父類構造器,虛擬機會保證在子類的<clinit>()方法執行之前,父類的<clinit>()方法已經執行完畢,因此在虛擬機中第一個被執行的<clinit>()方法的類肯定是Java.lang.object由於父類的<clinit>()方法先執行,也就意味着父類中定義的靜態語句塊要優先於子類的變量賦值操作。<clinit>()方法對於類或者接口來說並不是必須的,如果一個類中沒有靜態語句塊,也沒有對變量的賦值操作,那么編譯器可以不為這個類生產<clinit>()方法。接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操作,因此接口與類一樣都會生產<clinit>()方法。但是接口與類不同的是,執行接口的<clinit>()方法不需要先執行父接口的<clinit>()方法。只有當父接口中定義的變量被使用時,父接口才會初始化。
另外,接口的實現類在初始化時也一樣不會執行接口的<clinit>()方法。虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖同步,如果多個線程同時去初始化一個類,那么只會有一個線程去執行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。如果在一個類的<clinit>()方法中有耗時很長的操作,那么就可能造成多個進程阻塞。