基於 JDK8,面試常見題型,能夠先試試下面的牛刀小試面試題,而後再通讀全文,效果更佳~java
類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括面試
加載、驗證、準備、初始化和 卸載 這5個階段的順序是肯定的,類的加載過程必須按照這種順序循序漸進地開始,而後一般互相交叉地混合式進行,而解析階段則不必定:它在某些狀況下能夠在初始化階段以後再開始,這是爲了支持Java語言的運行時綁定(也稱爲動態綁定或晚期綁定)。編程
加載的過程當中主要作了3件事:bootstrap
對於一個類或者接口數組
有兩種類型的類加載器:安全
由 JVM 提供的 bootstrap 類加載器:用於加載 系統變量 sun.boot.class.path 所表明的路徑下的 class 文件,頂層父類網絡
用戶定義的類加載器(java 類庫中定義的也包括在內)數據結構
ClassLoader 使用 雙親委派模型 來加載類,每一個 ClassLoader 實例都持有父類加載器的引用,虛擬機內置的 bootstrap 類加載器爲頂層父類加載器,沒有父類加載器,但能夠做爲其它 ClassLoader 實例的父類加載器。當 ClassLoader 實例須要加載某個類時,它會先委派其父類加載器去加載。這個過程是由上至下依次檢查的,首先由最頂層的類加載器Bootstrap ClassLoader試圖加載,若是沒加載到,則把任務轉交給Extension ClassLoader試圖加載,若是也沒加載到,則轉交給App ClassLoader 進行加載,若是它也沒有加載獲得的話,則返回給委託的發起者,由它到指定的文件系統或網絡等URL中加載該類。若是它們都沒有加載到這個類時,則拋出ClassNotFoundException異常。不然將這個找到的類生成一個類的定義,並將它加載到內存當中,最後返回這個類在內存中的Class實例對象。oracle
雙親委派機制的優勢是能夠避免類的重複加載,當父類加載了子類就不必再加載。另外可以保證虛擬機的安全,防止內部實現類被自定義的類替換。函數
那麼JVM在搜索類的時候,如何判斷兩個 class 是否相同呢?答案是不只全類名要相同 ,並且還要由同一個類加載器實例加載。
主要確保類或接口的二進制表示在結構上是正確的。
準備工做包括爲類或接口建立靜態字段,並將這些字段初始化爲其默認值,這裏 不須要執行任何 java 代碼。
例如,對於類或接口中的以下靜態字段
private static int num = 666;
複製代碼
在準備階段會爲 num 設置默認值 0;在後面的初始化階段纔會給 num 賦值 666;
特殊狀況:對於同時被 static 和 final 修飾的 字段,準備階段就會賦值。
解析是將運行常量池中的符號引用 動態肯定爲具體值的過程。解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。
初始化階段是初始化類變量和其餘資源,或者說是執行類構造器<clinit>()方法的過程.
<clinit>()方法是由編譯器自動收集類中的全部==類變量==(static 修飾的變量)的賦值動做和靜態語句塊static{}中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句塊能夠賦值,可是不能訪問。
例如:非法向前引用變量示例
static {
i = 2;
System.out.println(i); //illegal forward reference
}
static int i = 4;
複製代碼
<clinit>()方法與類的構造函數(或者說實例構造器<init>()方法)不一樣,它不須要顯示地調用父類構造器,虛擬機會保證在子類的<clinit>()方法執行以前,父類的<clinit>()方法已經執行完畢。所以在虛擬機中第一個被執行的<clinit>()方法的類確定是 java.lang.Object。接口與類不一樣的是,執行接口的<clinit>()方法不須要先執行父接口的<clinit>()方法,只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也同樣不會執行接口的<clinit>()方法。
<clinit>()方法是類構造器,在類初始化的時候執行,用於 類中的靜態代碼塊 和 靜態字段 初始化的方法,只會執行一次。
<init>()方法是 類實例的構造器,在對象的初始化階段執行,用於非靜態字段,非靜態代碼塊,構造函數的初始化,能夠執行屢次。
類或者接口只能因爲如下緣由初始化:
執行以下代碼,輸出如何:
public class SSClass {
static
{
System.out.println("SSClass");
}
}
public class SuperClass extends SSClass {
static
{
System.out.println("SuperClass init!");
}
public static int value = 123;
public SuperClass() {
System.out.println("init SuperClass");
}
}
public class SubClass extends SuperClass {
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);
}
}
複製代碼
輸出:
SSClass
SuperClass init!
123
複製代碼
解析:
上面提到了這樣一句話:
從類中獲取 靜態字段、設置靜態字段、執行類中的靜態方法時,若是尚未被初始化,則 聲明該字段或者方法的類或者接口被初始化
因此 SubClass 類並不會被初始化,因此也就不會執行其 靜態代碼塊;
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;
}
複製代碼
問題:執行上面的程序,輸出結果是什麼?
答案:執行以上代碼的輸出結果:
2
3
a=110,b=0
1
4
複製代碼
解析:
執行 main 方法,會致使主類 StaticTest 初始化,因爲還未加載,先執行加載,主要分析 準備階段 和 初始化階段 的 賦值操做。
準備階段 : 爲靜態字段賦初始值,st 設爲 null ,b 設爲 0;
初始化階段:執行 Java 代碼的類構造器 <clinit>()方法,分別按順序執行以下代碼:
調用 staticFunction() 方法,System.out.println("4");
稍微修改一下代碼,去除 如下代碼,或許就變得正常多了,
static StaticTest st = new StaticTest();
複製代碼
輸出:
1
4
複製代碼
減小了 類實例化的步驟。
父類和子類的初始化順序能夠簡單用如下幾句話歸納:
參考:
深刻理解Java虛擬機
oracle 官方文檔
歡迎關注編程那點事兒,隨時隨地,想學就學,掃碼關注吧~