您好,登錄后才能下訂單哦!
這篇文章主要介紹“怎么理解Java類加載機制”,在日常操作中,相信很多人在怎么理解Java類加載機制問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”怎么理解Java類加載機制”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
在許多Java面試中,我們經常會看到關于Java類加載機制的考察,例如下面這道題:
class Grandpa{static{System.out.println("爺爺在靜態代碼塊");}} class Father extends Grandpa{static{System.out.println("爸爸在靜態代碼塊");}public static int factor = 25;public Father(){System.out.println("我是爸爸~");}}class Son extends Father{static {System.out.println("兒子在靜態代碼塊");}public Son(){System.out.println("我是兒子~");}}public class InitializationDemo{public static void main(String[] args){System.out.println("爸爸的歲數:" + Son.factor); //入口}}
請寫出最后的輸出字符串。
正確答案是:
爺爺在靜態代碼塊爸爸在靜態代碼塊爸爸的歲數:25
我相信很多同學看到這個題目之后,表情是崩潰的,完全不知道從何入手。有的甚至遇到了幾次,仍然無法找到正確的解答思路。
其實這種面試題考察的就是你對Java類加載機制的理解。
如果你對Java加載機制不理解,那么你是無法解答這道題目的。
所以這篇文章,我先帶大家學習Java類加載的基礎知識,然后再實戰分析幾道題目讓大家掌握思路。
下面我們先來學習下Java類加載機制的七個階段。
Java類加載機制的七個階段
當我們的Java代碼編譯完成后,會生成對應的 class 文件。接著我們運行java Demo命令的時候,我們其實是啟動了JVM 虛擬機執行 class 字節碼文件的內容。而 JVM 虛擬機執行 class 字節碼的過程可以分為七個階段:加載、驗證、準備、解析、初始化、使用、卸載。
加載
下面是對于加載過程最為官方的描述。
加載階段是類加載過程的第一個階段。在這個階段,JVM 的主要目的是將字節碼從各個位置(網絡、磁盤等)轉化為二進制字節流加載到內存中,接著會為這個類在 JVM 的方法區創建一個對應的 Class 對象,這個 Class 對象就是這個類各種數據的訪問入口。
其實加載階段用一句話來說就是:把代碼數據加載到內存中。這個過程對于我們解答這道問題沒有直接的關系,但這是類加載機制的一個過程,所以必須要提一下。
驗證
當 JVM 加載完 Class 字節碼文件并在方法區創建對應的 Class 對象之后,JVM 便會啟動對該字節碼流的校驗,只有符合 JVM 字節碼規范的文件才能被 JVM 正確執行。這個校驗過程大致可以分為下面幾個類型:
JVM規范校驗。JVM 會對字節流進行文件格式校驗,判斷其是否符合 JVM 規范,是否能被當前版本的虛擬機處理。例如:文件是否是以 0x cafe bene開頭,主次版本號是否在當前虛擬機處理范圍之內等。 代碼邏輯校驗。JVM 會對代碼組成的數據流和控制流進行校驗,確保 JVM 運行該字節碼文件后不會出現致命錯誤。例如一個方法要求傳入 int 類型的參數,但是使用它的時候卻傳入了一個 String 類型的參數。一個方法要求返回 String 類型的結果,但是最后卻沒有返回結果。代碼中引用了一個名為 Apple 的類,但是你實際上卻沒有定義 Apple 類。
當代碼數據被加載到內存中后,虛擬機就會對代碼數據進行校驗,看看這份代碼是不是真的按照JVM規范去寫的。這個過程對于我們解答問題也沒有直接的關系,但是了解類加載機制必須要知道有這個過程。
準備(重點)
當完成字節碼文件的校驗之后,JVM 便會開始為類變量分配內存并初始化。這里需要注意兩個關鍵點,即內存分配的對象以及初始化的類型。
內存分配的對象。Java 中的變量有「類變量」和「類成員變量」兩種類型,「類變量」指的是被 static 修飾的變量,而其他所有類型的變量都屬于「類成員變量」。在準備階段,JVM 只會為「類變量」分配內存,而不會為「類成員變量」分配內存。「類成員變量」的內存分配需要等到初始化階段才開始。
例如下面的代碼在準備階段,只會為 factor 屬性分配內存,而不會為 website 屬性分配內存。
public static int factor = 3;public String website = www.cnblogs.com/chanshuyi;
初始化的類型。在準備階段,JVM 會為類變量分配內存,并為其初始化。但是這里的初始化指的是為變量賦予 Java 語言中該數據類型的零值,而不是用戶代碼里初始化的值。
例如下面的代碼在準備階段之后,sector 的值將是 0,而不是 3。
public static int sector = 3;
但如果一個變量是常量(被 static final 修飾)的話,那么在準備階段,屬性便會被賦予用戶希望的值。例如下面的代碼在準備階段之后,number 的值將是 3,而不是 0。
public static final int number = 3;
之所以 static final 會直接被復制,而 static 變量會被賦予零值。其實我們稍微思考一下就能想明白了。
兩個語句的區別是一個有 final 關鍵字修飾,另外一個沒有。而 final 關鍵字在 Java 中代表不可改變的意思,意思就是說 number 的值一旦賦值就不會在改變了。既然一旦賦值就不會再改變,那么就必須一開始就給其賦予用戶想要的值,因此被 final 修飾的類變量在準備階段就會被賦予想要的值。而沒有被 final 修飾的類變量,其可能在初始化階段或者運行階段發生變化,所以就沒有必要在準備階段對它賦予用戶想要的值。
解析
當通過準備階段之后,JVM 針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符 7 類引用進行解析。這個階段的主要任務是將其在常量池中的符號引用替換成直接其在內存中的直接引用。
其實這個階段對于我們來說也是幾乎透明的,了解一下就好。
初始化(重點)
到了初始化階段,用戶定義的 Java 程序代碼才真正開始執行。在這個階段,JVM 會根據語句執行順序對類對象進行初始化,一般來說當 JVM 遇到下面 5 種情況的時候會觸發初始化:
遇到 new、getstatic、putstatic、invokestatic 這四條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯器把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。 使用 java.lang.reflect 包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。 當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。 當使用 JDK1.7 動態語言支持時,如果一個 java.lang.invoke.MethodHandle實例最后的解析結果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且這個方法句柄所對應的類沒有進行初始化,則需要先出觸發其初始化。
看到上面幾個條件你可能會暈了,但是不要緊,不需要背,知道一下就好,后面用到的時候回到找一下就可以了。
使用
當 JVM 完成初始化階段之后,JVM 便開始從入口方法開始執行用戶的程序代碼。這個階段也只是了解一下就可以。
卸載
當用戶程序代碼執行完畢后,JVM 便開始銷毀創建的 Class 對象,最后負責運行的 JVM 也退出內存。這個階段也只是了解一下就可以。
看完了Java的類加載機智之后,是不是有點懵呢。不怕,我們先通過一個小例子來醒醒神。
public class Book {public static void main(String[] args){System.out.println("Hello ShuYi.");}Book(){System.out.println("書的構造方法");System.out.println("price=" + price +",amount=" + amount);}{System.out.println("書的普通代碼塊");}int price = 110;static{System.out.println("書的靜態代碼塊");}static int amount = 112;}
思考一下上面這段代碼輸出什么?
給你5分鐘思考,5分鐘后交卷,哈哈。
怎么樣,想好了嗎,公布答案了。
書的靜態代碼塊Hello ShuYi.
怎么樣,你答對了嗎?是不是和你想得有點不一樣呢。
下面我們來簡單分析一下,首先根據上面說到的觸發初始化的5種情況的第4種(當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類),我們會進行類的初始化。
那么類的初始化順序到底是怎么樣的呢?
重點來了!
重點來了!
重點來了!
在我們代碼中,我們只知道有一個構造方法,但實際上Java代碼編譯成字節碼之后,是沒有構造方法的概念的,只有類初始化方法 和 對象初始化方法 。
那么這兩個方法是怎么來的呢?
類初始化方法。編譯器會按照其出現順序,收集類變量的賦值語句、靜態代碼塊,最終組成類初始化方法。類初始化方法一般在類初始化的時候執行。
上面的這個例子,其類初始化方法就是下面這段代碼了:
static{System.out.println("書的靜態代碼塊");}static int amount = 112;
對象初始化方法。編譯器會按照其出現順序,收集成員變量的賦值語句、普通代碼塊,最后收集構造函數的代碼,最終組成對象初始化方法。對象初始化方法一般在實例化類對象的時候執行。上面這個例子,其對象初始化方法就是下面這段代碼了:
{System.out.println("書的普通代碼塊");}int price = 110;System.out.println("書的構造方法");System.out.println("price=" + price +",amount=" + amount);
類初始化方法 和 對象初始化方法 之后,我們再來看這個例子,我們就不難得出上面的答案了。
但細心的朋友一定會發現,其實上面的這個例子其實沒有執行對象初始化方法。
因為我們確實沒有進行 Book 類對象的實例化。如果你在 main 方法中增加 new Book() 語句,你會發現對象的初始化方法執行了!
感興趣的朋友可以自己動手試一下,我這里就不執行了。
通過了上面的理論和簡單例子,我們下面進入更加復雜的實戰分析吧!
實戰分析
class Grandpa{static{System.out.println("爺爺在靜態代碼塊");}} class Father extends Grandpa{static{System.out.println("爸爸在靜態代碼塊");}public static int factor = 25;public Father(){System.out.println("我是爸爸~");}}class Son extends Father{static {System.out.println("兒子在靜態代碼塊");}public Son(){System.out.println("我是兒子~");}}public class InitializationDemo{public static void main(String[] args){System.out.println("爸爸的歲數:" + Son.factor); //入口}}
思考一下,上面的代碼最后的輸出結果是什么?
最終的輸出結果是:
爺爺在靜態代碼塊爸爸在靜態代碼塊爸爸的歲數:25
也許會有人問為什么沒有輸出「兒子在靜態代碼塊」這個字符串?
這是因為對于靜態字段,只有直接定義這個字段的類才會被初始化(執行靜態代碼塊)。因此通過其子類來引用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。
對面上面的這個例子,我們可以從入口開始分析一路分析下去:
首先程序到 main 方法這里,使用標準化輸出 Son 類中的 factor 類成員變量,但是 Son 類中并沒有定義這個類成員變量。于是往父類去找,我們在 Father 類中找到了對應的類成員變量,于是觸發了 Father 的初始化。 但根據我們上面說到的初始化的 5 種情況中的第 3 種(當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化)。我們需要先初始化 Father 類的父類,也就是先初始化 Grandpa 類再初始化 Father 類。于是我們先初始化 Grandpa 類輸出:「爺爺在靜態代碼塊」,再初始化 Father 類輸出:「爸爸在靜態代碼塊」。 最后,所有父類都初始化完成之后,Son 類才能調用父類的靜態變量,從而輸出:「爸爸的歲數:25」。
怎么樣,是不是覺得豁然開朗呢。
我們再來看一下一個更復雜點的例子,看看輸出結果是啥。
class Grandpa{static{System.out.println("爺爺在靜態代碼塊");}public Grandpa() {System.out.println("我是爺爺~");}}class Father extends Grandpa{static{System.out.println("爸爸在靜態代碼塊");}public Father(){System.out.println("我是爸爸~");}}class Son extends Father{static {System.out.println("兒子在靜態代碼塊");}public Son(){System.out.println("我是兒子~");}}public class InitializationDemo{public static void main(String[] args){new Son(); //入口}}
輸出結果是:
爺爺在靜態代碼塊爸爸在靜態代碼塊兒子在靜態代碼塊我是爺爺~我是爸爸~我是兒子~
怎么樣,是不是覺得這道題和上面的有所不同呢。
讓我們仔細來分析一下上面代碼的執行流程:
首先在入口這里我們實例化一個 Son 對象,因此會觸發 Son 類的初始化,而 Son 類的初始化又會帶動 Father 、Grandpa 類的初始化,從而執行對應類中的靜態代碼塊。因此會輸出:「爺爺在靜態代碼塊」、「爸爸在靜態代碼塊」、「兒子在靜態代碼塊」。 當 Son 類完成初始化之后,便會調用 Son 類的構造方法,而 Son 類構造方法的調用同樣會帶動 Father、Grandpa 類構造方法的調用,最后會輸出:「我是爺爺~」、「我是爸爸~」、「我是兒子~」。
看完了兩個例子之后,相信大家都胸有成足了吧。
下面給大家看一個特殊點的例子,有點難哦!
public class Book {public static void main(String[] args){staticFunction();}static Book book = new Book();static{System.out.println("書的靜態代碼塊");}{System.out.println("書的普通代碼塊");}Book(){System.out.println("書的構造方法");System.out.println("price=" + price +",amount=" + amount);}public static void staticFunction(){System.out.println("書的靜態方法");}int price = 110;static int amount = 112;}
上面這個例子的輸出結果是:
書的普通代碼塊書的構造方法price=110,amount=0書的靜態代碼塊書的靜態方法
下面我們一步步來分析一下代碼的整個執行流程。
在上面兩個例子中,因為 main 方法所在類并沒有多余的代碼,我們都直接忽略了 main 方法所在類的初始化。
但在這個例子中,main 方法所在類有許多代碼,我們就并不能直接忽略了。
當 JVM 在準備階段的時候,便會為類變量分配內存和進行初始化。此時,我們的 book 實例變量被初始化為 null,amount 變量被初始化為 0。 當進入初始化階段后,因為 Book 方法是程序的入口,根據我們上面說到的類初始化的五種情況的第四種(當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類)。所以JVM 會初始化 Book 類,即執行類構造器 。 JVM 對 Book 類進行初始化首先是執行類構造器(按順序收集類中所有靜態代碼塊和類變量賦值語句就組成了類構造器 ),后執行對象的構造器(按順序收集成員變量賦值和普通代碼塊,最后收集對象構造器,最終組成對象構造器 )。
對于 Book 類,其類構造方法()可以簡單表示如下:
static Book book = new Book();static{System.out.println("書的靜態代碼塊");}static int amount = 112;
于是首先執行static Book book = new Book();這一條語句,這條語句又觸發了類的實例化。于是 JVM 執行對象構造器 ,收集后的對象構造器 代碼:
{System.out.println("書的普通代碼塊");}int price = 110;Book(){System.out.println("書的構造方法");System.out.println("price=" + price +", amount=" + amount);}
于是此時 price 賦予 110 的值,輸出:「書的普通代碼塊」、「書的構造方法」。而此時 price 為 110 的值,而 amount 的賦值語句并未執行,所以只有在準備階段賦予的零值,所以之后輸出「price=110,amount=0」。
當類實例化完成之后,JVM 繼續進行類構造器的初始化:
static Book book = new Book(); //完成類實例化static{System.out.println("書的靜態代碼塊");}static int amount = 112;
即輸出:「書的靜態代碼塊」,之后對 amount 賦予 112 的值。
到這里,類的初始化已經完成,JVM 執行 main 方法的內容。
public static void main(String[] args){staticFunction();}
即輸出:「書的靜態方法」。
方法論
從上面幾個例子可以看出,分析一個類的執行順序大概可以按照如下步驟:
確定類變量的初始值。在類加載的準備階段,JVM 會為類變量初始化零值,這時候類變量會有一個初始的零值。如果是被 final 修飾的類變量,則直接會被初始成用戶想要的值。 初始化入口方法。當進入類加載的初始化階段后,JVM 會尋找整個 main 方法入口,從而初始化 main 方法所在的整個類。當需要對一個類進行初始化時,會首先初始化類構造器(),之后初始化對象構造器()。 初始化類構造器。JVM 會按順序收集類變量的賦值語句、靜態代碼塊,最終組成類構造器由 JVM 執行。 初始化對象構造器。JVM 會按照收集成員變量的賦值語句、普通代碼塊,最后收集構造方法,將它們組成對象構造器,最終由 JVM 執行。
如果在初始化 main 方法所在類的時候遇到了其他類的初始化,那么就先加載對應的類,加載完成之后返回。如此反復循環,最終返回 main 方法所在類。
到此,關于“怎么理解Java類加載機制”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。