首先引入一道面試題java
class Single {
private static Single single = new Single();
public static int count1;
public static int count2 = 0;
private Single() {
count1++;
count2++;
}
public static Single getInstance() {
return single;
}
}
public class Test {
public static void main(String[] args) {
Single single = Single.getInstance();
System.out.println("count1=" + single.count1);
System.out.println("count2=" + single.count2);
}
}
複製代碼
錯誤答案:
count1=1;count2=1
正確答案:count1=1;count2=0
面試
爲神馬?爲神馬?這要從java的類加載時機提及。數據庫
原本是準備把分析結果寫在最下面的可是怕你們沒有耐心看到最後我這邊先大概分析下,若是看不懂下面的分析。建議你們能看到最後,文章不算長。數組
Single single = Single.getInstance();
調用了類的Single
調用了類的靜態方法,觸發類的初始化single=null count1=0,count2=0
single
賦值爲new Single()
調用類的構造方法count=1;count2=1
count1
與count2
賦值,此時count1
沒有賦值操做,全部count1
爲1,可是count2
執行賦值操做就變爲0類從被加載到虛擬機內存中開始,直到卸載出內存爲止,它的整個生命週期包括了:加載、驗證、準備、解析、初始化、使用和卸載這7個階段。其中,驗證、準備和解析這三個部分統稱爲鏈接(linking)。 安全
其中加載、驗證、準備、初始化和卸載五個步驟的順序都是肯定的,解析階段在某些狀況下有可能發生在初始化以後,這是爲了支持 Java 語言的運行期綁定的特性。 ![]()
什麼狀況下須要開始類加載過程的第一個階段:"加載"。虛擬機規範中並沒強行約束,這點能夠交給虛擬機的的具體實現自由把握,可是對於初始化階段虛擬機規範是嚴格規定了以下幾種狀況,若是類未初始化會對類進行初始化。網絡
constant variable
),編譯器並不會生成字節碼來從對象中載入域的值,而是直接把這個值插入到字節碼中。這是一種頗有用的優化,可是若是你須要改變final域的值那麼每一塊用到那個域的代碼都須要從新編譯。Class.forName("my.xyz.Test")
)接口的加載過程與類的加載過程稍有不一樣。接口中不能使用
static{}
塊。當一個接口在初始化時,並不要求其父接口所有都完成了初始化,只有真正在使用到父接口時(例如引用接口中定義的常量)纔會初始化。數據結構
對於靜態字段,只有直接定義這個字段的類會被初始化,若是是經過子類引用父類的字段,父類會被初始化,子類不必定會被初始化,子類會不會被初始化 JVM 虛擬機規範並無明確規定,取決於虛擬機的具體實現多線程
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = 1;
}
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}
public class Demo {
public static void main(String[] args){
System.out.println("The value is " + Subclass.value);
}
}
複製代碼
上面代碼運行以後輸出結果以下所示編輯器
SuperClass init!
The value is 24
複製代碼
public class SubClass {
static {
System.out.println("SubClass init!");
}
}
public class Demo {
public static void main(String[] args){
SubClass[] subClassArray = new SubClass[10];
}
}
複製代碼
上面代碼運行以後,並不會輸出 "SubClass init!
",由於在上面Demo#main()
方法中,並無初始化SubClass
類,而是初始化了一個SubClass[]
數組類,SubClass[]
數組類表明了一個元素類型爲SubClass
的一維數組,繼承自Object
類,由newarray
字節碼建立。佈局
public class Constant {
static {
System.out.println("Constant init!");
}
public static final String VALUE = "Hello World!";
}
public class Demo {
public static void main(String[] args){
System.out.println(Constant.VALUE);
}
}
複製代碼
上面代碼運行以後也並不會輸出"Constant init!
",由於這涉及到一個概念 ---- 「常量傳播優化」。雖然在代碼中Demo
類引用了Constant
類中的常量VALUE
,可是在編譯階段,會將VALUE
的實際值"Hello World!
"放到Demo
類中的常量池中,Demo
類每次使用"Hello World!
"常量的時候都會從本身的常量池中去找。Demo
類不會持有Constant
類的符號引用,因此Constant
類也並不會被初始化。
在加載階段有三個步驟:
java.lang.Class
的對象,做爲方法區這些數據的訪問入口 在這個階段,有兩點須要注意:.class
靜態存儲文件中獲取,也能夠從zip、jar
等包中讀取,能夠從數據庫中讀取,也能夠從網絡中獲取,甚至咱們本身能夠在運行時自動生成。java.lang.Class
對象以後,並無規定此Class
對象是方法Java
堆中的,有些虛擬機就會將Class
對象放到方法區中,好比HotSpot
。驗證是鏈接階段的第一個步驟,驗證的目的是爲了確保.class
文件中的字節流所包含的信息是符合當前虛擬機的要求,而且不會危害到虛擬機自身的安全的。
Java
語言自己是相對安全的語言,使用Java編碼是沒法作到如訪問數組邊界之外的數據、將一個對象轉型爲它並未實現的類型等,若是這樣作了,編譯器將拒絕編譯。可是,Class
文件並不必定是由Java
源碼編譯而來,可使用任何途徑,包括用十六進制編輯器(如UltraEdit
)直接編寫。若是直接編寫了有害的「代碼」(字節流),而虛擬機在加載該Class時不進行檢查的話,就有可能危害到虛擬機或程序的安全。
不一樣的虛擬機,對類驗證的實現可能有所不一樣,但大體都會完成下面四個階段的驗證:文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證。
private、protected、public、default
)是否可被當前類訪問驗證階段對於虛擬機的類加載機制來講,不必定是必要的階段。若是所運行的所有代碼確認是安全的,可使用-Xverify:none
參數來關閉大部分的類驗證措施,以縮短虛擬機類加載時間。
準備階段是爲類的靜態變量分配內存並將其初始化爲默認值,這些內存都將在方法區中進行分配。準備階段不分配類中的實例變量的內存,實例變量將會在對象實例化時隨着對象一塊兒分配在Java堆中。
有三點須要注意:
static
修飾的變量),而不包括實例變量,實例變量將會跟隨着對象在 Java 堆中爲其分配內存0
值,好比有以下類變量,在準備階段完成以後val
的值是0
而不是 123
,爲 val
複製爲123
,是在後面要講的初始化階段以後public static int val=123;//在準備階段value初始值爲0 。在初始化階段纔會變爲123 。
複製代碼
ConstantValue
屬性當中,因此在準備階段結束以後,常量的值就是ConstantValue
所指定的值了,好比以下,在準備階段結束以後,val
的值就是123
了。public static final int val = 123;
複製代碼
解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。 符號引用(Symbolic Reference):符號引用以一組符號來描述所引用的目標,符號能夠是任何形式的字面量,只要使用時能無歧義地定位到目標便可。符號引用與虛擬機實現的內存佈局無關,引用的目標並不必定已經加載到內存中。 直接引用(Direct Reference):直接引用能夠是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。直接引用是與虛擬機實現的內存佈局相關的,若是有了直接引用,那麼引用的目標一定已經在內存中存在。
類的初始化階段纔是真正開始執行類中定義的 Java 程序代碼。初始化說白了就是調用類構造器<clinit>()
的過程,在類的構造器中會爲類變量初始化定義的值,會執行靜態代碼塊中的內容。下面將介紹幾點和開發者關係較爲緊密的注意點
<clinit>()
是由編譯器自動收集類中出現的類變量、靜態代碼塊中的語句合併產生的,收集的順序是在源文件中出現的順序決定的,靜態代碼塊能夠訪問出如今靜態代碼塊以前的類變量,出現的靜態代碼塊以後的類變量,只能夠賦值,可是不能訪問,好比以下代碼public class Demo {
private static String before = "before";
static {
after = "after"; // 賦值合法
System.out.println(before); // 訪問合法,由於出如今 static{} 以前
System.out.println(after); // 訪問不合法,由於出如今 static{} 以後
}
private static String after;
}
複製代碼
<clinit>()
類構造器和<init>()
實例構造器不一樣,類構造器不須要顯示的父類的類構造,在子類的類構造器調用以前,會自動的調用父類的類構造器。所以虛擬機中第一個被調用的<clinit>()
方法是 java.lang.Object
的類構造器static{}
代碼塊也優先於子類的static{}
執行<clinit>()
對於類來講並非必需的,若是一個類中沒有類變量,也沒有static{}
,那這個類不會有類構造器<clinit>()
static{}
,可是接口中也能夠有類變量,因此接口中也能夠有類構造器 <clinit>{}
,可是接口的類構造器和類的類構造器有所不一樣,接口在調用類構造器的時候,若是不須要,不用調用父接口的類構造器,除非用到了父接口中的類變量,接口的實現類在初始化的時候也不會調用接口的類構造器<clinit>()
方法在多線程環境中被正確地加鎖、同步,若是多個線程同時去初始化一個類,那麼只有一個線程去執行這個類的類構造器<clinit>()
,其餘線程會被阻塞,直到活動線程執行完類構造器<clinit>()
方法看到這裏不容易了,你們應該都理解類加載的流程了吧,但願之後遇到這樣的面試題能想起這篇文章
能夠和博主一塊兒交流: