在許多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); //入口
}
}
複製代碼
請寫出最後的輸出字符串。web
正確答案是:面試
爺爺在靜態代碼塊
爸爸在靜態代碼塊
爸爸的歲數:25
複製代碼
我相信不少同窗看到這個題目以後,表情是崩潰的,徹底不知道從何入手。有的甚至遇到了幾回,仍然沒法找到正確的解答思路。bash
**其實這種面試題考察的就是你對Java類加載機制的理解。**若是你對Java加載機制不理解,那麼你是沒法解答這道題目的。這篇文章,我將經過對Java類加載機制的講解,讓你掌握解答此類題目的方法。網絡
當咱們的Java代碼編譯完成後,會生成對應的 class 文件。接着咱們運行java Demo命令的時候,咱們實際上是啓動了JVM 虛擬機執行 class 字節碼文件的內容。而 JVM 虛擬機執行 class 字節碼的過程能夠分爲七個階段:加載、驗證、準備、解析、初始化、使用、卸載。測試
下面是對於加載過程最爲官方的描述。spa
加載階段是類加載過程的第一個階段。在這個階段,JVM 的主要目的是將字節碼從各個位置(網絡、磁盤等)轉化爲二進制字節流加載到內存中,接着會爲這個類在 JVM 的方法區建立一個對應的 Class 對象,這個 Class 對象就是這個類各類數據的訪問入口。code
其實加載階段用一句話來講就是:**把代碼數據加載到內存中。**這個過程對於咱們解答這道問題沒有直接的關係,但這是類加載機制的一個過程,因此必需要提一下。cdn
當 JVM 加載完 Class 字節碼文件並在方法區建立對應的 Class 對象以後,JVM 便會啓動對該字節碼流的校驗,只有符合 JVM 字節碼規範的文件才能被 JVM 正確執行。這個校驗過程大體能夠分爲下面幾個類型:對象
當代碼數據被加載到內存中後,虛擬機就會對代碼數據進行校驗,看看這份代碼是否是真的按照JVM規範去寫的。這個過程對於咱們解答問題也沒有直接的關係,可是瞭解類加載機制必需要知道有這個過程。
當完成字節碼文件的校驗以後,JVM 便會開始爲類變量分配內存並初始化。這裏須要注意兩個關鍵點,即內存分配的對象以及初始化的類型。
例以下面的代碼在準備階段,只會爲 factor 屬性分配內存,而不會爲 website 屬性分配內存。
public static int factor = 3;
public String website = "www.cnblogs.com/chanshuyi";
複製代碼
例以下面的代碼在準備階段以後,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 種狀況的時候會觸發初始化:
看到上面幾個條件你可能會暈了,可是沒關係,不須要背,知道一下就好,後面用到的時候回到找一下就能夠了。
當 JVM 完成初始化階段以後,JVM 便開始從入口方法開始執行用戶的程序代碼。這個階段也只是瞭解一下就能夠。
當用戶程序代碼執行完畢後,JVM 便開始銷燬建立的 Class 對象,最後負責運行的 JVM 也退出內存。這個階段也只是瞭解一下就能夠。
瞭解了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
複製代碼
也許會有人問爲何沒有輸出「兒子在靜態代碼塊」這個字符串?
這是由於對於靜態字段,只有直接定義這個字段的類纔會被初始化(執行靜態代碼塊),所以經過其子類來引用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。
對面上面的這個例子,咱們能夠從入口開始分析一路分析下去:
咱們再來看一下下面這個例子,看看輸出結果是啥。
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 對象,可是當子類初始化時會帶動父類初始化,所以輸出結果就如上面所示。
咱們仔細來分析一下上面代碼的執行流程:
下面咱們舉一個稍微複雜點的例子。
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
書的靜態代碼塊
書的靜態方法
複製代碼
下面咱們一步步來分析一下代碼的整個執行流程。
對於 Book 類,其類構造方法能夠簡單表示以下:
static Book book = new Book();
static
{
System.out.println("書的靜態代碼塊");
}
static int amount = 112;
複製代碼
因而首先執行**static Book book = new Book();**這一條語句,這條語句又觸發了類的實例化。與類構造器不一樣,因而 JVM 執行 Book 類的成員變量,再蒐集普通代碼塊,最後執行類的構造方法,因而其執行語句能夠表示以下:
int price = 110;
{
System.out.println("書的普通代碼塊");
}
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 的值。
public static void main(String[] args)
{
staticFunction();
}
複製代碼
即輸出:「書的靜態方法」。
從上面幾個例子能夠看出,分析一個類的執行順序大概能夠按照以下步驟:
看完了上面的解析以後,再去看看開頭那道題是否是以爲簡單多了呢。不少東西就是這樣,掌握了必定的方法和知識以後,本來困難的東西也變得簡單許多了。