jvm类加载过程_JVM类生命周期概述:加载时机与加载过程
作者:菜鳥小于
https://www.cnblogs.com/Young111/p/11359700.html
? ? 一個.java文件在編譯后會形成相應的一個或多個Class文件,這些Class文件中描述了類的各種信息,并且它們最終都需要被加載到虛擬機中才能被運行和使用。事實上,虛擬機把描述類的數據從Class文件加載到內存,并對數據進行校驗,轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型的過程就是虛擬機的類加載機制。本文概述了JVM加載類的時機和生命周期,并結合典型案例重點介紹了類的初始化過程,進而了解JVM類加載機制。
一、類加載機制概述
我們知道,一個.java文件在編譯后會形成相應的一個或多個Class文件(若一個類中含有內部類,則編譯后會產生多個Class文件),但這些Class文件中描述的各種信息,最終都需要加載到虛擬機中之后才能被運行和使用。事實上,虛擬機把描述類的數據從Class文件加載到內存,并對數據進行校驗,轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型的過程就是虛擬機的 類加載機制。
與那些在編譯時需要進行連接工作的語言不同,在Java語言里面,類型的加載和連接都是在程序運行期間完成,這樣會在類加載時稍微增加一些性能開銷,但是卻能為Java應用程序提供高度的靈活性,Java中天生可以動態擴展的語言特性多態就是依賴運行期動態加載和動態鏈接這個特點實現的。例如,如果編寫一個使用接口的應用程序,可以等到運行時再指定其實際的實現。這種組裝應用程序的方式廣泛應用于Java程序之中。
既然這樣,那么,
虛擬機什么時候才會加載Class文件并初始化類呢?(類加載和初始化時機)
虛擬機如何加載一個Class文件呢?(Java類加載的方式:類加載器、雙親委派機制)
虛擬機加載一個Class文件要經歷那些具體的步驟呢?(類加載過程/步驟)
本文主要對第一個和第三個問題進行闡述。
二. 類加載的時機
Java類從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命周期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using) 和 卸載(Unloading)七個階段。其中準備、驗證、解析3個部分統稱為連接(Linking),如圖所示:
加載、驗證、準備、初始化和卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開始,這是為了支持Java語言的運行時綁定(也稱為動態綁定或晚期綁定)。以下陳述的內容都已HotSpot為基準。特別需要注意的是,類的加載過程必須按照這種順序按部就班地“開始”,而不是按部就班的“進行”或“完成”,因為這些階段通常都是相互交叉地混合式進行的,也就是說通常會在一個階段執行的過程中調用或激活另外一個階段。
了解了Java類的生命周期以后,那么我們現在來回答第一個問題:虛擬機什么時候才會加載Class文件并初始化類呢?
1、類加載時機
什么情況下虛擬機需要開始加載一個類呢?虛擬機規范中并沒有對此進行強制約束,這點可以交給虛擬機的具體實現來自由把握。
2、類初始化時機
那么,什么情況下虛擬機需要開始初始化一個類呢?這在虛擬機規范中是有嚴格規定的,虛擬機規范指明 有且只有 五種情況必須立即對類進行初始化(而這一過程自然發生在加載、驗證、準備之后):
1) 遇到new、getstatic、putstatic或invokestatic這四條字節碼指令(注意,newarray指令觸發的只是數組類型本身的初始化,而不會導致其相關類型的初始化,比如,new String[]只會直接觸發String[]類的初始化,也就是觸發對類[Ljava.lang.String的初始化,而直接不會觸發String類的初始化)時,如果類沒有進行過初始化,則需要先對其進行初始化。生成這四條指令的最常見的Java代碼場景是:
使用new關鍵字實例化對象的時候;
讀取或設置一個類的靜態字段(被final修飾,已在編譯器把結果放入常量池的靜態字段除外)的時候;
調用一個類的靜態方法的時候。
2) 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
3) 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
4) 當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
5) 當使用jdk1.7動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且這個方法句柄所對應的類沒有進行初始化,則需要先出觸發其初始化。
注意,對于這五種會觸發類進行初始化的場景,虛擬機規范中使用了一個很強烈的限定語:“有且只有”,這五種場景中的行為稱為對一個類進行 主動引用。除此之外,所有引用類的方式,都不會觸發初始化,稱為 被動引用。
特別需要指出的是,類的實例化與類的初始化是兩個完全不同的概念:
類的實例化是指創建一個類的實例(對象)的過程;
類的初始化是指為類中各個類成員(被static修飾的成員變量)賦初始值的過程,是類生命周期中的一個階段。
3、被動引用的幾種經典場景
1)、通過子類引用父類的靜態字段,不會導致子類初始化
public class SSClass{ static{ System.out.println("SSClass"); }} public class SClass extends SSClass{ static{ System.out.println("SClass init!"); } public static int value = 123; public SClass(){ System.out.println("init SClass"); }}public class SubClass extends SClass{ static{ System.out.println("SubClass init"); } static int a; public SubClass(){ System.out.println("init SubClass"); }}public class NotInitialization{ public static void main(String[] args){ System.out.println(SubClass.value); }}/* Output: SSClass SClass init! */對于靜態字段,只有直接定義這個字段的類才會被初始化,因此通過其子類來引用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。在本例中,由于value字段是在類SClass中定義的,因此該類會被初始化;此外,在初始化類SClass時,虛擬機會發現其父類SSClass還未被初始化,因此虛擬機將先初始化父類SSClass,然后初始化子類SClass,而SubClass始終不會被初始化。
2)、通過數組定義來引用類,不會觸發此類的初始化
public class NotInitialization{ public static void main(String[] args){ SClass[] sca = new SClass[10]; }}3)、常量在編譯階段會存入調用類的常量池中,本質上并沒有直接引用到定義常量的類,因此不會觸發定義常量的類的初始化
public class ConstClass{ static{ System.out.println("ConstClass init!"); } public static final String CONSTANT = "hello world";}public class NotInitialization{ public static void main(String[] args){ System.out.println(ConstClass.CONSTANT); }}/* Output: hello world */上述代碼運行之后,只輸出 “hello world”,這是因為雖然在Java源碼中引用了ConstClass類中的常量CONSTANT,但是編譯階段將此常量的值“hello world”存儲到了NotInitialization常量池中,對常量ConstClass.CONSTANT的引用實際都被轉化為NotInitialization類對自身常量池的引用了。也就是說,實際上NotInitialization的Class文件之中并沒有ConstClass類的符號引用入口,這兩個類在編譯為Class文件之后就不存在關系了。
三. 類加載過程
如上圖所示,我們在上文已經提到過一個類的生命周期包括加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using) 和 卸載(Unloading)七個階段。現在我們一一學習一下JVM在加載、驗證、準備、解析和初始化五個階段是如何對每個類進行操作的。1、加載
加載是類加載過程中的一個階段,?這個階段會在內存中生成一個代表這個類的 java.lang.Class 對象,?作為方法區這個類的各種數據的入口。注意這里不一定非得要從一個 Class 文件獲取,這里既可以從 ZIP 包中讀取(比如從 jar 包和 war 包中讀取),也可以在運行時計算生成(動態代理),也可以由其它文件生成(比如將 JSP 文件轉換成對應的 Class 類)。?
2、驗證
這一階段的主要目的是為了確保 Class 文件的字節流中包含的信息是否符合當前虛擬機的要求,并且不會危害虛擬機自身的安全。
3、準備
準備階段是正式為類變量分配內存并設置類變量的初始值階段,即在方法區中分配這些變量所使用的內存空間。注意這里所說的初始值概念,比如一個類變量定義為?
public static int v = 8080;實際上變量 v 在準備階段過后的初始值為 0 而不是 8080,?將 v 賦值為 8080 的 put static 指令是程序被編譯后,?存放于類構造器方法之中。但是注意如果聲明為?
public static final int v = 8080;在編譯階段會為 v 生成 ConstantValue 屬性,在準備階段虛擬機會根據 ConstantValue 屬性將 v賦值為 8080。?
4、解析
解析階段是指虛擬機將常量池中的符號引用替換為直接引用的過程。符號引用就是 class 文件中的:
1.?CONSTANT_Class_info
2.?CONSTANT_Field_info
3.?CONSTANT_Method_info等類型的常量。?
4.1?符號引用
? 符號引用與虛擬機實現的布局無關,?引用的目標并不一定要已經加載到內存中。?各種虛擬機實現的內存布局可以各不相同,但是它們能接受的符號引用必須是一致的,因為符號引用的字面量形式明確定義在 Java 虛擬機規范的 Class 文件格式中?
?4.2?直接引用
?直接引用可以是指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄。如果有了直接引用,那引用的目標必定已經在內存中存在。?
5、初始化
初始化階段是類加載最后一個階段,前面的類加載階段之后,除了在加載階段可以自定義類加載器以外,其它操作都由 JVM 主導。到了初始階段,才開始真正執行類中定義的 Java 程序代碼 。初始化階段是執行類構造器方法的過程。 方法是由編譯器自動收集類中的類變量的賦值操作和靜態語句塊中的語句合并而成的。虛擬機會保證子方法執行之前,父類的方法已經執行完畢,?如果一個類中沒有對靜態變量賦值也沒有靜態語句塊,那么編譯器可以不為這個類生成()方法?
?注意以下幾種情況不會執行類初始化:
1.?通過子類引用父類的靜態字段,只會觸發父類的初始化,而不會觸發子類的初始化。 2.?定義對象數組,不會觸發該類的初始化。 3.?常量在編譯期間會存入調用類的常量池中,本質上并沒有直接引用定義常量的類,不會觸 ? ?發定義常量所在的類。 4.?通過類名獲取 Class 對象,不會觸發類的初始化。 5.?通過 Class.forName 加載指定類時,如果指定參數 initialize 為 false 時,也不會觸發類初 始化,其實這個參數是告訴虛擬機,是否要對類進行初始化。 6.?通過 ClassLoader 默認的 loadClass 方法,也不會觸發初始化動作。?
? 虛擬機會保證一個類的類構造器()在多線程環境中被正確的加鎖、同步,如果多個線程同時去初始化一個類,那么只會有一個線程去執行這個類的類構造器(),其他線程都需要阻塞等待,直到活動線程執行()方法完畢。特別需要注意的是,在這種情形下,其他線程雖然會被阻塞,但如果執行()方法的那條線程退出后,其他線程在喚醒之后不會再次進入/執行()方法,因為 在同一個類加載器下,一個類型只會被初始化一次。如果在一個類的()方法中有耗時很長的操作,就可能造成多個線程阻塞,在實際應用中這種阻塞往往是隱藏的,如下所示:
public class DealLoopTest { static{ System.out.println("DealLoopTest..."); } static class DeadLoopClass { static { if (true) { System.out.println(Thread.currentThread() + "init DeadLoopClass"); while (true) { // 模擬耗時很長的操作 } } } } public static void main(String[] args) { Runnable script = new Runnable() { // 匿名內部類 public void run() { System.out.println(Thread.currentThread() + " start"); DeadLoopClass dlc = new DeadLoopClass(); System.out.println(Thread.currentThread() + " run over"); } }; Thread thread1 = new Thread(script); Thread thread2 = new Thread(script); thread1.start(); thread2.start(); }}/* Output: DealLoopTest... Thread[Thread-1,5,main] start Thread[Thread-0,5,main] start Thread[Thread-1,5,main]init DeadLoopClass */如上述代碼所示,在初始化DeadLoopClass類時,線程Thread-1得到執行并在執行這個類的類構造器() 時,由于該方法包含一個死循環,因此久久不能退出。
四. 典型案例分析
在Java中, 創建一個對象常常需要經歷如下幾個過程:父類的類構造器() -> 子類的類構造器() -> 父類的成員變量和實例代碼塊 -> 父類的構造函數 -> 子類的成員變量和實例代碼塊 -> 子類的構造函數。
那么,我們看看下面的程序的輸出結果:
public class StaticTest { public static void main(String[] args) { staticFunction(); } static StaticTest st = new StaticTest(); static { //靜態代碼塊 System.out.println("1"); } { // 實例代碼塊 System.out.println("2"); } StaticTest() { // 實例構造器 System.out.println("3"); System.out.println("a=" + a + ",b=" + b); } public static void staticFunction() { // 靜態方法 System.out.println("4"); } int a = 110; // 實例變量 static int b = 112; // 靜態變量}/* Output: 3 a=110,b=0 4 */大家能得到正確答案嗎?雖然筆者勉強猜出了正確答案,但總感覺怪怪的。因為在初始化階段,當JVM對類StaticTest進行初始化時,首先會執行下面的語句:
static StaticTest st = new StaticTest();也就是實例化StaticTest對象,但這個時候類都沒有初始化完畢啊,能直接進行實例化嗎?事實上,這涉及到一個根本問題就是:實例初始化不一定要在類初始化結束之后才開始初始化。 下面我們結合類的加載過程說明這個問題。
我們知道,類的生命周期是:加載->驗證->準備->解析->初始化->使用->卸載,并且只有在準備階段和初始化階段才會涉及類變量的初始化和賦值,因此我們只針對這兩個階段進行分析:
首先,在類的準備階段需要做的是為類變量(static變量)分配內存并設置默認值(零值),因此在該階段結束后,類變量st將變為null、b變為0。特別需要注意的是,如果類變量是final的,那么編譯器在編譯時就會為value生成ConstantValue屬性,并在準備階段虛擬機就會根據ConstantValue的設置將變量設置為指定的值。也就是說,如果上述程度對變量b采用如下定義方式時:
static final int b=112那么,在準備階段b的值就是112,而不再是0了。
此外,在類的初始化階段需要做的是執行類構造器(),需要指出的是,類構造器本質上是編譯器收集所有靜態語句塊和類變量的賦值語句按語句在源碼中的順序合并生成類構造器()。因此,對上述程序而言,JVM將先執行第一條靜態變量的賦值語句:
st = new StaticTest ();在類都沒有初始化完畢之前,能直接進行實例化相應的對象嗎?
事實上,從Java角度看,我們知道一個類初始化的基本常識,那就是:在同一個類加載器下,一個類型只會被初始化一次。所以,一旦開始初始化一個類型,無論是否完成,后續都不會再重新觸發該類型的初始化階段了(只考慮在同一個類加載器下的情形)。因此,在實例化上述程序中的st變量時,實際上是把實例初始化嵌入到了靜態初始化流程中,并且在上面的程序中,嵌入到了靜態初始化的起始位置。這就導致了實例初始化完全發生在靜態初始化之前,當然,這也是導致a為110b為0的原因。
因此,上述程序的StaticTest類構造器()的實現等價于:
public class StaticTest { (){ a = 110; // 實例變量 System.out.println("2"); // 實例代碼塊 System.out.println("3"); // 實例構造器中代碼的執行 System.out.println("a=" + a + ",b=" + b); // 實例構造器中代碼的執行 類變量st被初始化 System.out.println("1"); //靜態代碼塊 類變量b被初始化為112 }}因此,上述程序會有上面的輸出結果。下面,我們對上述程序稍作改動,在程序最后的一行,增加以下代碼行:
static StaticTest st1 = new StaticTest();那么,此時程序的輸出又是什么呢?如果你對上述的內容理解很好的話,不難得出結論(只有執行完上述代碼行后,StaticTest類才被初始化完成),即:
3a=110,b=02a=110,b=112那么下面的程序的執行結果是什么呢???
class Foo { int i = 1; Foo() { System.out.println(i); int x = getValue(); System.out.println(x); } { i = 2; } protected int getValue() { return i; }}//子類class Bar extends Foo { int j = 1; Bar() { j = 2; } { j = 3; } @Override protected int getValue() { return j; }}public class ConstructorExample { public static void main(String... args) { Bar bar = new Bar(); System.out.println(bar.getValue()); }}在創建對象前,先進行類的初始化,類的初始化會將所有非靜態代碼塊收集起來先執行,而父類必須先于子類初始化,所以父類靜態代碼塊先執行,接著是子類靜態代碼塊。此時類初始化完成。接下來要創建子類實例,子類通過super()調用父類構造方法,在執行構造方法之前要先執行非靜態代碼塊,所以順序是 父類非靜態代碼塊 》 父類構造函數 》 子類非靜態代碼塊 》 子類構造函數
運行程序,就知道結果。只要真正理解類的實例化過程,這類問題不會再難道我們了!
長按關注鋒哥微信公眾號,非常感謝;
總結
以上是生活随笔為你收集整理的jvm类加载过程_JVM类生命周期概述:加载时机与加载过程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java switch中if_详解jav
- 下一篇: 爬get接口_网络字体反爬之起点中文小说