有這樣一道面試題:html
class Singleton{ private static Singleton singleton = new Singleton(); public static int value1; public static int value2 = 0; private Singleton(){ value1++; value2++; } public static Singleton getInstance(){ return singleton; } } class Singleton2{ public static int value1; public static int value2 = 0; private static Singleton2 singleton2 = new Singleton2(); private Singleton2(){ value1++; value2++; } public static Singleton2 getInstance2(){ return singleton2; } } public static void main(String[] args) { Singleton singleton = Singleton.getInstance(); System.out.println("Singleton1 value1:" + singleton.value1); System.out.println("Singleton1 value2:" + singleton.value2); Singleton2 singleton2 = Singleton2.getInstance2(); System.out.println("Singleton2 value1:" + singleton2.value1); System.out.println("Singleton2 value2:" + singleton2.value2); }
說出運行的結果:
Singleton1 value1 : 1
Singleton1 value2 : 0
Singleton2 value1 : 1
Singleton2 value2 : 1java
稍後會帶來分析。web
JVM類加載分爲5個過程:加載,驗證,準備,解析,初始化,使用,卸載,以下圖所示:
面試
下面來看看加載,驗證,準備,解析,初始化這5個過程的具體動做。網絡
加載主要是將.class文件(並不必定是.class。能夠是ZIP包,網絡中獲取)中的二進制字節流讀入到JVM中。
在加載階段,JVM須要完成3件事:
1)經過類的全限定名獲取該類的二進制字節流;
2)將字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構;
3)在內存中生成一個該類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口。數據結構
驗證是鏈接階段的第一步,主要確保加載進來的字節流符合JVM規範。
驗證階段會完成如下4個階段的檢驗動做:
1)文件格式驗證
2)元數據驗證(是否符合Java語言規範)
3)字節碼驗證(肯定程序語義合法,符合邏輯)
4)符號引用驗證(確保下一步的解析能正常執行)架構
準備是鏈接階段的第二步,主要爲靜態變量在方法區分配內存,並設置默認初始值。spa
解析是鏈接階段的第三步,是虛擬機將常量池內的符號引用替換爲直接引用的過程。.net
初始化階段是類加載過程的最後一步,主要是根據程序中的賦值語句主動爲類變量賦值。
注:
1)當有父類且父類爲初始化的時候,先去初始化父類;
2)再進行子類初始化語句。線程
何時須要對類進行初始化?
1)使用new該類實例化對象的時候;
2)讀取或設置類靜態字段的時候(但被final修飾的字段,在編譯器時就被放入常量池的靜態字段除外static final);
3)調用類靜態方法的時候;
4)使用反射Class.forName(「xxxx」)對類進行反射調用的時候,該類須要初始化;
5) 初始化一個類的時候,有父類,先初始化父類(注:1. 接口除外,父接口在調用的時候纔會被初始化;2.子類引用父類靜態字段,只會引起父類初始化);
6) 被標明爲啓動類的類(即包含main()方法的類)要初始化;
7)當使用JDK1.7的動態語言支持時,若是一個java.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化。
以上狀況稱爲對一個類進行主動引用,且有且只要以上幾種狀況須要對類進行初始化。
再回過頭來分析一開始的面試題:
Singleton輸出結果:1 0
緣由:
1 首先執行main中的Singleton singleton = Singleton.getInstance();
2 類的加載:加載類Singleton
3 類的驗證
4 類的準備:爲靜態變量分配內存,設置默認值。這裏爲singleton(引用類型)設置爲null,value1,value2(基本數據類型)設置默認值0
5 類的初始化(按照賦值語句進行修改):
執行private static Singleton singleton = new Singleton();
執行Singleton的構造器:value1++;value2++; 此時value1,value2均等於1
執行
public static int value1;
public static int value2 = 0;
此時value1=1,value2=0
Singleton2輸出結果:1 1
緣由:
1 首先執行main中的Singleton2 singleton2 = Singleton2.getInstance2();
2 類的加載:加載類Singleton2
3 類的驗證
4 類的準備:爲靜態變量分配內存,設置默認值。這裏爲value1,value2(基本數據類型)設置默認值0,singleton2(引用類型)設置爲null,
5 類的初始化(按照賦值語句進行修改):
執行
public static int value2 = 0;
此時value2=0(value1不變,依然是0);
執行
private static Singleton singleton = new Singleton();
執行Singleton2的構造器:value1++;value2++;
此時value1,value2均等於1,即爲最後結果
類加載器實現的功能是即爲加載階段獲取二進制字節流的時候。
JVM提供瞭如下3種系統的類加載器:
類加載器之間的層次關係以下:
照片來源:http://www.importnew.com/25295.html
類加載器之間的這種層次關係叫作雙親委派模型。
雙親委派模型要求除了頂層的啓動類加載器(Bootstrap ClassLoader)外,其他的類加載器都應當有本身的父類加載器。這裏的類加載器之間的父子關係通常不是以繼承關係實現的,而是用組合實現的。
若是一個類接受到類加載請求,他本身不會去加載這個請求,而是將這個類加載請求委派給父類加載器,這樣一層一層傳送,直到到達啓動類加載器(Bootstrap ClassLoader)。
只有當父類加載器沒法加載這個請求時,子加載器纔會嘗試本身去加載。
雙親委派模型的代碼實現集中在java.lang.ClassLoader的loadClass()方法當中。
1)首先檢查類是否被加載,沒有則調用父類加載器的loadClass()方法;
2)若父類加載器爲空,則默認使用啓動類加載器做爲父加載器;
3)若父類加載失敗,拋出ClassNotFoundException 異常後,再調用本身的findClass() 方法。
loadClass源代碼以下:
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { //1 首先檢查類是否被加載 Class c = findLoadedClass(name); if (c == null) { try { if (parent != null) { //2 沒有則調用父類加載器的loadClass()方法; c = parent.loadClass(name, false); } else { //3 若父類加載器爲空,則默認使用啓動類加載器做爲父加載器; c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { //4 若父類加載失敗,拋出ClassNotFoundException 異常後 c = findClass(name); } } if (resolve) { //5 再調用本身的findClass() 方法。 resolveClass(c); } return c; }
雙親委派模型很好的解決了各個類加載器加載基礎類的統一性問題。即越基礎的類由越上層的加載器進行加載。
若加載的基礎類中須要回調用戶代碼,而這時頂層的類加載器沒法識別這些用戶代碼,怎麼辦呢?這時就須要破壞雙親委派模型了。
下面介紹兩個例子來說解破壞雙親委派模型的過程。
JNDI破壞雙親委派模型
JNDI是Java標準服務,它的代碼由啓動類加載器去加載。可是JNDI須要回調獨立廠商實現的代碼,而類加載器沒法識別這些回調代碼(SPI)。
爲了解決這個問題,引入了一個線程上下文類加載器。 可經過Thread.setContextClassLoader()設置。
利用線程上下文類加載器去加載所須要的SPI代碼,即父類加載器請求子類加載器去完成類加載的過程,而破壞了雙親委派模型。
Spring破壞雙親委派模型
Spring要對用戶程序進行組織和管理,而用戶程序通常放在WEB-INF目錄下,由WebAppClassLoader類加載器加載,而Spring由Common類加載器或Shared類加載器加載。
那麼Spring是如何訪問WEB-INF下的用戶程序呢?
使用線程上下文類加載器。 Spring加載類所用的classLoader都是經過Thread.currentThread().getContextClassLoader()獲取的。當線程建立時會默認建立一個AppClassLoader類加載器(對應Tomcat中的WebAppclassLoader類加載器): setContextClassLoader(AppClassLoader)。
利用這個來加載用戶程序。即任何一個線程均可經過getContextClassLoader()獲取到WebAppclassLoader。
(圖片來源:http://lib.csdn.net/article/java/60356)
Tomcat目錄下有4組目錄:
參考:《深刻理解Java虛擬機》