類的生命週期
類的載入階段
載入完成的操作
載入的理解
所謂載入,簡而言之就是將Java類的位元組碼檔案載入到機器記憶體中,並在記憶體中構建出Java類的原型―類别範本物件。所謂類别範本物件,其實就是Java類在JVA記憶體中的一個快照,JVM將從位元組碼檔案中解析出的常量池、類欄位、類方法等資訊儲存到類别範本中,這樣JVW在執行期便能通過類别範本而獲取Java類中的任意資訊,能夠對Java類的成員變數進行遍歷,也能進行Java方法的呼叫。|
反射的機制即基於這一基礎。如果JVM沒有將Java類的宣告資訊儲存起來,則JVM在執行期也無法反射。
載入完成的操作
載入階段,簡言之,查詢並載入類的二進位制資料, 生成Class的例項。
在載入類Java虛擬機器必須完成以下3件事情:
●通過類的全名,獲取類的二進位制資料流。
●解析類的二進位制資料流為方法區內的資料結構(Java類模型)|
●建立java.lang. Class類的例項, 表示該型別。作為方法區這個類的各種資料的訪問入口
二進位制流的獲取方式
對於類的二進位制資料流,虛擬機器可以通過多種途徑產生或獲得。 (只要所讀取的位元組碼符合JVM規範即可)
1.虛擬機器可能通過檔案系統讀入一個class字尾的檔案(最常見)
2.讀入jar、zip等歸檔資料包,提取類檔案。
3.事先存放在資料庫中的類的二進位制資料
4.使用類似於HTTP之類的協議通過網路進行載入
5.在執行時生成一段Class的二進位制資訊等
在獲取到類的二進位制資訊後,Java虛擬機器就會處理這些資料,並最終轉為一個java . lang. Class的例項。如果輸入資料不是ClassFile的結構, 則會丟擲ClassFormatError.
類模型與Class例項的位置
1.類模型的位置
載入的類在JVM中建立相應的類結構,類結構會儲存在方法區(JDK1.8之前:永久代:3DK1.8及之後。元空間).
2.Class例項的位置
類將.class檔案載入至元空間後,會在堆中建立一個Java.lang.Class物件,用來封裝類位於方法區內的資料結構,該Class物件是在載入類的過程中建立的,每個類都對應有一個class型別的物件。
再說明
class類的構造方法是私有的,只有JVM能夠建立。
java.lang.class例項是訪問型別後設資料的介面,也是實現反射的關鍵資料、入口。通過Class類提供的介面,可以獲得目標類所關聯的.class檔案中具體的資料結構:方法、欄位等資訊。
反射機制程式碼
public static void main(String[] args) {
try {
Class clazz = Class.forName("java.lang.String");
//獲取當前執行時類宣告的所有方法
Method[] ms = clazz.getDeclaredMethods();
for (Method m : ms) {
//獲取方法的修飾符
String mod = Modifier.toString(m.getModifiers());
System.out.print(mod + " ");
//獲取方法的返回值型別
String returnType = m.getReturnType().getSimpleName();
System.out.print(returnType + " ");
//獲取方法名
System.out.print(m.getName() + "(");
//獲取方法的引數列表
Class<?>[] ps = m.getParameterTypes();
if (ps.length == 0) System.out.print(')');
for (int i = 0; i < ps.length; i++) {
char end = (i == ps.length - 1) ? ')' : ',';
//獲取引數的型別
System.out.print(ps[i].getSimpleName() + end);
}
System.out.println();
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
陣列類的載入
建立陣列類的情況稍微有些特殊,因為陣列類本身並不是由類載入器負責建立,而是由JVW在執行時根據需要而直接建立的,但陣列的元素型別仍然需要依靠類載入器去建立。建立陣列類(下述簡稱A)的過程:
1.如果陣列的元素型別是引用型別,那麼就遵循定義的載入過程遞迴載入和建立陣列A的元素型別;
2.JVM使用指定的元素型別和陣列維度來建立新的陣列類。
如果陣列的元素型別是引用型別,陣列類的可訪問性就由元素型別的可訪問性決定。否則陣列類的可訪問性將被預設定義為public.
類的連結階段
驗證階段(Verification)
當類載入到系統後,就開始連結操作,驗證是連結操作的第一步。
它的目的是保證載入的位元組碼是合法、合理並符合規範的。
驗證的步驟比較複雜,實際要驗證的專案也很繁多,大體上Java虛擬機器需要做以下檢查,如圖所示。
整體說明:
驗證的內容則涵蓋了類資料資訊的格式驗證、語義檢查、位元組碼驗證,以及符號引用驗證等。
●其中格式驗證會和載入階段一起執行。驗證通過之後,類載入器才會成功將類的二進位制資料資訊載入到方法區中。.格式驗證之外的驗證操作將會在方法區中進行。
連結階段的驗證雖然拖慢了載入速度,但是它避免了在位元組碼執行時還需要進行各種檢查。
具體說明:
1.格式驗證:是否以魔數OxCAFEBABE開頭,主版本和副版本號是否在當前Java虛擬機器的支援範圍內,資料中每一個項是否都擁有正確的長度等。
2. Java虛擬機器會進行位元組碼的語義檢查,但凡在語義上不符合規範的,虛擬機器也不會給子驗證通過。比如:
●是否所有的類都有父類的存在(在Java裡,除了object外,其他類都應該有父類)
●是否一些被定義為final的方法或者類被重寫或繼承了
●非抽象類是否實現了所有抽象方法或者介面方法
●是否存在不相容的方法(比如方法的簽名除了返回值不同,其他都一樣,這種方法會讓虛擬機器無從下手排程: abstract情況下的方法,就不能是final的了)
3. Java虛擬機器還會進行位元組碼驗證,位元組碼驗證也是驗證過程中最為複雜的一個過程。它試圖通過對位元組碼流的分析,判斷位元組碼是否可以被正確地執行。比如:
●在位元組碼的執行過程中,是否會跳轉到條不存在的指令
●函式的呼叫是否傳遞了正確型別的引數
●變數的賦值是不是給了正確的資料型別等
棧對映幀(StackMapTable)就是在這個階段,用於檢測在特定的位元組碼處,其區域性變數表和運算元棧是否有著正確的資料型別。但遺憾的是,100%準確地判斷一段位元組碼是否可以被安全執行是無法實現的,因此,該過程只是儘可能地檢查出可以預知的明顯的問題。如果在這個階段無法通過檢查,虛擬機器也不會正確裝載這個類。但是,如果通過了這個階段的檢查,也不能說明這個類是完全沒有問題的。
在前面3次檢查中,已經排除了檔案格式錯誤、語義錯誤以及位元組碼的不正確性。但是依然不能確保類是沒有問題的。
4.校驗器還將進行符號引用的驗證。class檔案在其常量池會通過字串記錄自己將要使用的其他類或者方法。因此,在驗證階段,虛擬機器就會檢查這些類或者方法確實是存在的,並且當前類有許可權訪問這些資料,如果一個需要使用類無法在系統中找到,則會丟擲NoClassDefFoundError,如果一個方法無法被找到,則會丟擲NoSuchMethodError。此階段在解析環節才會執行。
連結階段之Preparation(準備)
準備階段(Preparation),簡言之,為類的靜態變數分配記憶體,並將其初始化為預設值。
當一個類驗證通過時,虛擬機器就會進入準備階段。在這個階段,虛擬機器就會為這個類分配相應的記憶體空間,並設定預設初始值.Java虛擬機器為各型別變數預設的初始值如表所示。
注意:
1.這裡不包含基本資料型別的欄位用static final修飾的情況,因為final在編譯的時候就會分配了,準備階段會顯式賦值。
2.注意這裡不會為例項變數分配初始化,類變數會分配在方法區中,而例項變數是會隨著物件一起分配到Java堆中。
3.在這個階段並不會像初始化階段中那樣會有初始化或者程式碼被執行。
補充
基本資料型別:非final修飾的變數,在準備環節進行預設初始化賦值。
final修飾以後,在準備環節直接進行顯示賦值。
擴充:如果使用字面量的方式定義一個字串的常量的話,也是在準備環節直接進行顯示賦值。
連結階段之Resolution(解析)
在準備階段完成後,就進入瞭解析階段。
解析階段(Resolution),簡言之,將類、介面、欄位和方法的符號引用轉為直接引用。
1.具體描述:
符號引用就是一些字面量的引用,和虛擬機器的內部資料結構和和記憶體佈局無關。比較容易理解的就是在Class類檔案中,通過常量池進行了大量的符號引用。但是在程式實際執行時,只有符號引用是不夠的,比如當如下println()方法被呼叫時,系統需要明確知道該方法的位置。
舉例:輸出操作System.out.print1n()對應的位元組碼:invokevirtual #24 <java/io/PrintStream.print1n>
以方法為例,Java虛擬機器為每個類都準備了一張方法表,將其所有的方法都列在表中,當需要呼叫一個類的方法的時候,只要知道這個方法在方法表中的偏移量就可以直接呼叫該方法。通過解析操作,符號引用就可以轉變為目標方法在類中方法表中的位置,從而使得方法被成功呼叫。
2.小結:
所謂解析就是將符號引用轉為直接引用,也就是得到類、欄位、方法在記憶體中的指標或者偏移量。因此,可以說,如果直接引用存在,那麼可以肯定系統中存在該類、方法或者欄位。但只存在符號引用,不能確定系統中一定存在該結構。
不過Java虛擬機器規範並沒有明確要求解析階段一定要按照順序執行。在HotSpot VW中,載入、驗證、準備和初始化會按照順序有條不紊地執行,但連結階段中的解析操作往往會伴隨著JVM在執行完初始化之後再執行。
3,字串的複習
最後,再來看一下CONSTANT_String的解析。由於字串在程式開發中有著重要的作用,因此,讀者有必要了解一下String在Java虛擬機器中的處理。當在Java程式碼中直接使用字串常量時,就會在類中出現CONSTANT_String,它表示字串常量,並且會引用一個CONSTANT_UTF8的常量項。在Java虛擬機器內部執行中的常量池中,會維護一張字串拘留表(intern),它會儲存所有出現過的字串常量,並且沒有重複項。只要以CONSTANT_String形式出現的字串也都會在這張表中。使用String.intern()方法可以得到一個字串在拘留表中的引用,因為該表中沒有重複項,所以任何字面相同的字串的String.intern()方法返回總是相等的。
lnitialization(初始化)階段
初始化階段,簡言之,為類的靜態變數賦予正確的初始值。
1.具體描述
類的初始化是類裝載的最後一個階段。 如果前面的步驟都沒有問題,那麼表示類可以順利裝載到系統中。此時,類才會開始執行Java位元組碼。(即: 到了初始化階段,才真正開始執行類中定義的Java 程式程式碼。)
初始化階段的重要工作是執行類的初始化方法:()方法。
●該方法僅能由Java編譯器生成並由JVM呼叫,程式開發者無法自定義一個同名的方法,更無法直接在Java程式中呼叫該方法,雖然該方法也是由位元組碼指令所組成。
●它是由類靜態成員的賦值語句以及static語句塊合併產生的。
2.說明
2.1在載入一個類之前,虛擬機器總是會試圖載入該類的父類,因此父類的總是在子類之前被呼叫。也就是說,父類的static塊優先順序高於子類。
2.2 Java編譯器並不會為所有的類都產生< clinit>()初始化方法。哪些類在編譯為位元組碼後,位元組碼檔案中將不會包含< clinit>()方法?
●一個類中並沒有宣告任何的類變數,也沒有靜態程式碼塊時
●一個類中宣告類變數,但是沒有明確使用類變數的初始化語句以及靜態程式碼塊來執行初始化操作時
●一個類中包含static final修飾的基本資料型別的欄位,這些類欄位初始化語句採用編譯時常量表示式
相關程式碼
**
* @author shkstart
* @create 2020-09-14 18:49
* 哪些場景下,java編譯器就不會生成<clinit>()方法
*/
public class InitializationTest1 {
//場景1:對應非靜態的欄位,不管是否進行了顯式賦值,都不會生成<clinit>()方法
public int num = 1;
//場景2:靜態的欄位,沒有顯式的賦值,不會生成<clinit>()方法
public static int num1;
//場景3:比如對於宣告為static final的基本資料型別的欄位,不管是否進行了顯式賦值,都不會生成<clinit>()方法
public static final int num2 = 1;
}
/
/**
* @author shkstart
* @create 2020-09-14 18:55
*
* 說明:使用static + final修飾的欄位的顯式賦值的操作,到底是在哪個階段進行的賦值?
* 情況1:在連結階段的準備環節賦值
* 情況2:在初始化階段<clinit>()中賦值
*
* 結論:
* 在連結階段的準備環節賦值的情況:
* 1. 對於基本資料型別的欄位來說,如果使用static final修飾,則顯式賦值(直接賦值常量,而非呼叫方法)通常是在連結階段的準備環節進行
* 2. 對於String來說,如果使用字面量的方式賦值,使用static final修飾的話,則顯式賦值通常是在連結階段的準備環節進行
*
* 在初始化階段<clinit>()中賦值的情況:
* 排除上述的在準備環節賦值的情況之外的情況。
*
* 最終結論:使用static + final修飾,且顯示賦值中不涉及到方法或構造器呼叫的基本資料型別或String型別的顯式賦值,是在連結階段的準備環節進行。
*/
public class InitializationTest2 {
public static int a = 1;//在初始化階段<clinit>()中賦值
public static final int INT_CONSTANT = 10;//在連結階段的準備環節賦值
public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100);//在初始化階段<clinit>()中賦值
public static Integer INTEGER_CONSTANT2 = Integer.valueOf(1000);//在初始化階段<clinit>()中賦值
public static final String s0 = "helloworld0";//在連結階段的準備環節賦值
public static final String s1 = new String("helloworld1");//在初始化階段<clinit>()中賦值
public static String s2 = "helloworld2";
public static final int NUM1 = new Random().nextInt(10);//在初始化階段<clinit>()中賦值
}
< clinit>的執行緒安全性
對於< clinit>()方法的呼叫,也就是類的初始化,虛擬機器會在內部確保其多執行緒環境中的安全性。
虛擬機器會保證一個類的< clinit>()方法在多執行緒環境中被正確地加鎖、同步,如果多個執行緒同時去初始化一個類,那麼只會有一個執行緒去執行這個類的< clinit>()方法,其他執行緒都需要阻塞等待,直到活動執行緒執行< clinit>()方法完畢。
正是因為函式< clinit>()帶鎖執行緒安全的,因此,如果在一個類的cclinit>()方法中有耗時很長的操作,就可能造成多個執行緒阻塞,引發死鎖。並且這種死鎖是很難發現的,因為看起來它們並沒有可用的鎖資訊。
如果之前的執行緒成功載入了類,則等在佇列中的執行緒就沒有機會再執行< clinit>()方法了。那麼,當需要使用這個類時,虛擬機器會直接返回給它已經準備好的資訊。
死鎖例子
class StaticA {
static {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
try {
Class.forName("com.atguigu.java1.StaticB");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("StaticA init OK");
}
}
class StaticB {
static {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
try {
Class.forName("com.atguigu.java1.StaticA");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("StaticB init OK");
}
}
public class StaticDeadLockMain extends Thread {
private char flag;
public StaticDeadLockMain(char flag) {
this.flag = flag;
this.setName("Thread" + flag);
}
@Override
public void run() {
try {
Class.forName("com.atguigu.java1.Static" + flag);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println(getName() + " over");
}
public static void main(String[] args) throws InterruptedException {
StaticDeadLockMain loadA = new StaticDeadLockMain('A');
loadA.start();
StaticDeadLockMain loadB = new StaticDeadLockMain('B');
loadB.start();
}
}
類的初始化情況:主動使用vs被動使用
一、主動使用 會呼叫方法
Class只有在必須要首次使用的時候才會被裝載,Java虛擬機器不會無條件地裝載Class型別。Java虛擬機器規定,一個類或介面在初次使用前,必須要進行初始化。這裡指的“使用”,是指主動使用,主動使用只有下列幾種情況:(即:如果出現如下的情況,則會對類進行初始化操作。而初始化操作之前的載入、驗證、準備已經完成。)
1.當建立一個類的例項時,比如使用new關鍵字,或者通過反射、克隆、反序列化。
2.當呼叫類的靜態方法時,即當使用了位元組碼invokestatic指令。
3.當使用類、介面的靜態欄位時(final修飾特殊考慮),比如,使用getstatic或者putstatic指令。(對應訪問變數、賦值變數操作)
4.當使用java.lang.reflect包中的方法反射類的方法時。比如:Class.forName(“com.atguigu.java.Test”)
5.當初始化子類時,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
6.如果一個介面定義了default方法,那麼直接實現或者間接實現該介面的類的初始化,該介面要在其之前被初始化。
7,當虛擬機器啟動時,使用者需要指定一個要執行的主類〈包含main()方法的那個類),虛擬機器會先初始化這個主類。
8.當初次呼叫MethodHandle例項時,初始化該MethodHandle指向的方法所在的類。(涉及解析
REF_getStatic、REF_putstatic、REF_invokeStatic方法控制程式碼對應的類)
針對5,補充說明:
當Java虛擬機器初始化一個類時,要求它的所有父類都已經被初始化,但是這條規則並不適用於介面。
在初始化一個類時,並不會先初始化它所實現的介面
在初始化一個介面時,並不會先初始化它的父介面
因此,一個父介面並不會因為它的子介面或者實現類的初始化而初始化。只有當程式首次使用特定介面的靜態欄位時,才會導致該介面的初始化。
針對7,說明:
JVM啟動的時候通過引導類載入器載入一個初始類。這個類在呼叫public static void main(String[])方法之前被連結和初始化。這個方法的執行將依次導致所需的類的載入,連結和初始化。
引數-XX:+TraceClassLoading可看載入欣喜
二、被動使用 不會呼叫方法
除了以上的情況屬於主動使用,其他的情況均屬於被動使用。被動使用不會引起類的初始化。
也就是說:並不是在程式碼中出現的類,就一定會被載入或者初始化。如果不符合主動使用的條件,類就不會初始化。
1.當訪問一個靜態欄位時,只有真正宣告這個欄位的類才會被初始化。
·當通過子類引用父類的靜態變數,不會導致子類初始化
2.通過陣列定義類引用,不會觸發此類的初始化
3.引用常量不會觸發此類或介面的初始化。因為常量在連結階段就已經被顯式賦值了。
4.呼叫ClassLoader類的loadClass()方法載入一個類,並不是對類的主動使用,不會導致類的初始化。例如:
過程四:類的Using(使用)
開發人員可以在程式中訪問和呼叫它的靜態類成員資訊(比如:靜態欄位、靜態方法),或者使用new關鍵字為其建立物件例項。
類的Unloading(解除安裝)
一、型類的載入器、類的例項之間的引用關係
在類載入器的內部實現中,用一個Java集合來存放所載入類的引用。另一方面,一個Class物件總是會引用它的類載入器,呼叫Class物件的getClassLoader()方法,就能獲得它的類載入器。由此可見,代表某個類的Class例項與其類的載入器之間為雙向關聯關係。
一個類的例項總是引用代表這個類的Class物件。在Object類中定義了getClass()方法,這個方法返回代表物件所屬類的Class物件的引用。此外,所有的Java類都有一個靜態屬性class,它引用代表這個類的Class物件。
二、類的生命週期
當Sample類被載入、連結和初始化後,它的生命週期就開始了。當代表Sample類的Class物件不再被引用,即不可觸及時,Class物件就會結束生命週期,Sample類在方法區內的資料也會被解除安裝,從而結束Sample類的生命週期。
一個類何時結束生命週期,取決於代表它的Class物件何時結束生命週期。
如果程式執行過程中,將上圖左側三個引用變數都置為null,此時Sample物件結束生命週期,MyClassLoader物件結束生命週期,代表Sample類的C1ass物件也結束生命週期,Sample類在方法區內的二進位制資料被解除安裝。
當再次有需要時,會檢查Sample類的Class物件是否存在,如果存在會直接使用,不再重新載入。如果不存在Sample類會被重新載入,在Java虛擬機器的堆區會生成一個新的代表Sample類的Class例項(可以通過雜湊碼檢視是否是同一個例項).
四、類的解除安裝
(1)啟動類載入器載入的型別在整個執行期間是不可能被解除安裝的(jvm和jls規範)
(2)被系統類載入器和擴充套件類載入器載入的型別在執行期間不太可能被解除安裝,因為系統類載入器例項或者擴充套件類的例項基本上在整個執行期間總能直接或者間接的訪問的到,其達到unreachable的可能性極小。
(3)被開發者自定義的類載入器例項載入的型別只有在很簡單的上下文環境中才能被解除安裝,而且一般還要藉助於強制呼叫虛擬機器的垃圾收集功能才可以做到。可以預想,稍微複雜點的應用場景中(比如:很多時候使用者在開發自定義類載入器例項的時候採用快取的策略以提高系統效能),被載入的型別在執行期間也是幾乎不太可能被解除安裝的(至少解除安裝的時間是不確定的)。
綜合以上三點,一個已經載入的型別被解除安裝的機率很小至少被解除安裝的時間是不確定的。同時我們可以看的出來,開發者在開發程式碼時候,不應該對虛擬機器的型別解除安裝做任何假設的前提下,來實現系統中的特定功能。
相關文章
- Java類的生命週期淺析Java
- View生命週期與Activity生命週期的關係View
- 生命週期
- 聊聊React v16.3的UNSAFE類生命週期React
- Spring的生命週期Spring
- bean的生命週期Bean
- Servlet的生命週期Servlet
- SQL的生命週期SQL
- UIViewController的生命週期UIViewController
- Flutter 的生命週期Flutter
- Laravel的生命週期Laravel
- viewController的生命週期ViewController
- 品牌生命週期和產品生命週期之間的關係
- ubuntu生命週期Ubuntu
- vue生命週期Vue
- spring生命週期Spring
- Flutter - 生命週期Flutter
- sessionStorag 生命週期Session
- PHP 生命週期PHP
- maven生命週期Maven
- Activity生命週期
- React生命週期React
- vue - 生命週期Vue
- Fragment生命週期Fragment
- Spring的生命週期主Spring
- Avalonia的Window生命週期
- Flutter widget的生命週期Flutter
- Spring Bean的生命週期SpringBean
- Spring Bean 的生命週期SpringBean
- React新的生命週期React
- iOS APP的生命週期iOSAPP
- Vue生命週期的理解Vue
- Java 物件的生命週期Java物件
- Salesforce 生命週期管理(一)應用生命週期淺談Salesforce
- Vue的生命週期的理解Vue
- 講述: Java類的生命週期,各路高手前來指正~Java
- UIAbility元件生命週期UI元件
- IOC - bean 生命週期Bean