對於類加載的第一個階段—--加載,虛擬機沒有強制的約束,可是對於初始化階段,虛擬機強制規定有且只有如下的5中狀況必須開始初始化,固然,加載、驗證、準備階段在初始化前就已經開始。java
①使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾靜態字段除外)的時候,以及調用一個類的靜態方法的時候。程序員
②對類進行反射調用的時候數據庫
③當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化。api
④當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。數組
⑤當使用JDK 1.7的動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,而且這個方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化。tomcat
上面的5種場景中的行爲稱爲對一個類的主動引用。除此以外的其餘引用,虛擬機都不會觸發類的初始化,稱爲被動引用。安全
被動引用的例子:服務器
/* *經過子類引用父類的靜態字段,不會致使子類初始化 **/ public class SuperClass { static { System.out.println("SuperClass init!"); } public static int value = 123; } public class SubClass extends SuperClass{ static{ System.out.println("SubClass init!"); } }
/** *非主動使用類字段演示 **/ public class NotInitialization{ public static void main(String[]args){ System.out.println(SubClass.value); } }
結果:只會輸出SuperClass init!網絡
/** *被動使用類字段演示二: *經過數組定義來引用類,不會觸發此類的初始化 **/ public class NotInitialization{ public static void main(String[]args){ SuperClass[]sca=new SuperClass[10]; } }
結果:不會輸出SuperClass init!數據結構
/** *非主動使用類字段演示 **/ public class NotInitialization{ public static void main(String[]args){ System.out.println(ConstClass.HELLOWORLD); } }
結果:沒有輸出「ConstClass init!」
接口加載過程:只與類加載過程當中的第三條不一樣,當一個類在初始化時,要求其父類所有都已經初始化過了,可是一個接口在初始化時,並不要求其父接口所有都完成了初始化,只有在真正使用到父接口的時候(如引用接口中定義的常量)纔會初始化。
加載階段虛擬機要完成三件事:
①經過一個類的全限定名來獲取定義此類的二進制字節流。
②將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
③在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口。
驗證階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。虛擬機主要作如下工做:
①文件格式驗證:例如:以魔數0xCAFEBABE開頭
②元數據驗證:例如:是否有父類
③字節碼驗證:對類的方法體驗證
④符號引用驗證:例如:經過全限定名是否能找到對應的類
準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。如:
public static int value=123;
準備階段只會把類變量的初始值(也就是0)賦值給value,而真正到初始化階段才把123賦值給value。
注意:若是是final修飾的字符串,虛擬機會在準備階段就對變量作初始化。以下:
public static final int value=123;
編譯時Javac將會爲value生成ConstantValue屬性,在準備階段虛擬機就會根據ConstantValue的設置將value賦值爲123。
解析階段解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行解析。
在此以前的全部動做都是虛擬機來主導的,到了初始化階段才真正執行類中定義的Java代碼。也就是說由程序員寫的用來初始化類變量或其餘資源的代碼在初始化階段才得以執行。
事實上,初始化過程就是是執行類構造器<clinit>()方法的過程,<clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊(static{}塊)中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的。
靜態語句塊中只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句塊能夠賦值,可是不能訪問。以下代碼:
//非法向前引用變量: public class Test{ static{ i=0;//給變量賦值能夠正常編譯經過 System.out.print(i);//這句編譯器會提示"非法向前引用" } static int i=1; }
經過一個類的全限定名(如:反射)來獲取描述此類的二進制字節流類加載器,這個動做的代碼模塊稱爲類加載器。
判斷兩個類是否相等:
①類名是否相同,包括包名;
②是否由同一個類加載器加載,只有在兩個類由同一個類加載器加載的前提下才有意義,不然即便這兩個類都來源於同一個class文件,而加載這個class文件的類加載器不一樣,那麼這兩個類也是不相等的。
虛擬機提供了3種類加載器,引導(Bootstrap)類加載器、擴展(Extension)類加載器、系統(System)類加載器(也稱應用類加載器)。
啓動類加載器:<JAVA_HOME>/lib路徑下的核心類庫或-Xbootclasspath參數指定的路徑下的jar包,出於安全考慮,只加載包名爲java、javax、sun等開頭的類。
擴展類加載器:加載<JAVA_HOME>/lib/ext目錄下或者由系統變量-Djava.ext.dir指定位路徑中的類庫
應用程序類加載器:也稱它爲系統類加載器。加載系統類路徑java -classpath或-D java.class.path 指定路徑下的類庫,也就是咱們常常用到的classpath路徑,開發者能夠直接使用系統類加載器。
當類加載器加載一個類的時候,它先委託其父類加載器加載這個類,一樣的,父類加載器也委託其父類加載器加載這個類,一直到頂層的啓動類加載器,啓動類加載器默認是沒有父類的,當啓動類加載器沒法加載此類時,就會委派給其子類加載器,一樣的,一直委派下去,直到有子類加載器能成功加載此類,不然就報錯。這就是雙親委派模式。
使用雙親委派模式的優點:
①避免類的重複加載:當父類已經加載了這個類,子類就沒有必要再加載一次。
②防止核心api庫被篡改:當從網絡中傳過來一個java.lang.Object類時,類加載器經過雙親委派模式委派到啓動類加載器,啓動類加載器發現這個類已經加載了,就不會再去加載傳過來的這個類,而是直接返回已經加載的類。
破壞雙親委派模式:
①使用線程上下文類加載器,當基礎類要調用回用戶的代碼,這時因爲雙親委派模式的存在,啓動類加載器並不能委派給子類加載器去加載類,此時就要用到線程上下文類加載器來解決。常見有JNDI、JDBC、JCE、JAXB和JBI等。
以jdbc爲例:<JAVA_HOME>/lib下已經封裝好了jdbc的接口,由各個不一樣的數據庫廠商按照接口的規範來編寫這些接口的實現類,可是這些實現類一般都是放在classpath路徑下,啓動類加載器須要調用這些接口的實現類,但它並不能加載classpath路徑下的類,classpath路徑下的類只能由系統類加載器加載,又因爲雙親委派模式的存在,只能由子類加載器委託父類加載器加載類,不能由父類加載器委派給子類加載器加載類。因而便出了一個折中的辦法,當啓動類加載器須要調用這些接口的實現類時,它委託線程上下文類加載器去加載這些實現類。默認狀況下,線程上下文類加載器就是系統類加載器。
②代碼熱替換(HotSwap)、模塊熱部署(Hot Deployment):
意思就是但願應用程序能像咱們的計算機外設那樣,接上鼠標、U盤,不用重啓機器就能當即使用,鼠標有問題或要升級就換個鼠標,不用停機也不用重啓。具體應用例子有Tomcat下的項目的自動發佈,jsp修改後無需重啓服務器等。
Common類加載器:加載tomcat路徑下的lib目錄下class文件
Webapp類加載器:加載tomcat路徑下的/WebApp/WEB-INF/*下class文件
Jsp類加載器:它的加載範圍僅僅是這個JSP文件所編譯出來的那一個Class,它出現的目的就是爲了被丟棄:當服務器檢測到JSP文件被修改時,會替換掉目前的JasperLoader的實例,並經過再創建一個新的Jsp類加載器來實現JSP文件的HotSwap功能。
WebApp類加載器和Jsp類加載器一般會存在多個實例,每個Web應用程序對應一個WebApp類加載器,每個JSP文件對應一個Jsp類加載器。