關於類加載機制的知識,先簡要了解一下虛擬機(JVM)。當我們使用eclipse或者命令行調用命令javac.exe運行Java程序時,系統就會啓動一個虛擬機把類加載進內存中,類加載的過程就是需要了解的類加載機制。
虛擬機特點:
- 每啓動一次Java程序,都會單獨啓動一個JVM進程來運行;
- JVM是一個進程,JVM的數據不是共享的;
- Java程序結束後,JVM進程也會結束,同時JVM內存區的所有狀態全部丟失,類存儲在內存區中的數據會迴歸原始狀態。
內存區數據表示的是數據存儲在內存中,而非對象持久化或者進行數據庫操作那種,那兩種的數據都不是存儲在內存區。例如使用office編寫文檔,不保存的話,當電腦死機或者軟件崩潰時會導致文檔內容消失。
在兩個主類中分別寫兩個死循環,依次運行,在任務管理器中能看到兩個JVM進程(javaw.exe)在運行。
測試JVM結束後內存區狀態是否丟失案例:
public class Test01 {
public static int num = 10;
public static void main(String[] args) {
num = 20;
System.out.println("Test01的num值:" + num);
}
}
public class Test02 {
public static void main(String[] args) {
Test01 t1 = new Test01();
System.out.println("" + Test01.num);
}
}
- 案例說明:創建兩個主類,先運行Test01類,對num再次賦值20,打印輸出20,執行結束,JVM進程結束。運行Test02類,調用Test01的num值,重新初始化Test01類,打印輸出10。該案例簡單驗證了JVM結束後內存區狀態會丟失。
類的加載機制
- 加載---->驗證---->準備---->解析----->初始化---->使用----->卸載(驗證、準備、解析屬於連接階段)
加載階段
- 通過一個類的全限定名來獲取定義此類的二進制字節流;
- 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構;
- 在java堆中生成一個代表這個類的Class對象,作爲訪問方法區中這些數據的入口(反射機制就需要使用Class實例)。
- 因爲沒有指明從哪裏獲取以及怎樣獲取類的二進制字節流,所以可以自定義類加載器來控制加載階段。
連接階段
- 類加載之後,系統會生成一個對應的Class對象,接着進入連接階段,該階段爲類變量分配內存和設置默認初始值,這個內存分配發生在方法區中。
- 該階段還沒有對實例變量進行內存分配,實例變量會在對象實例化時隨着對象一起分配在JAVA堆中。
初始化階段
- 初始化階段是對類變量進行初始化,如果聲明類變量時沒有指定初始值,那使用系統分配的默認值;
- 類變量初始化有兩種方式:1、聲明類變量時指定值,2、在靜態代碼塊中爲類變量指定值;
- 初始化一個類時,會先對該類的父類初始化,然後依次類推,直到所有父類資源都被初始化完成。所以Object類總是第一個初始化。
注意:靜態初始化塊和聲明類變量都是初始化語句,執行順序是誰在前面誰先執行,但兩者都是屬於類本身,所以比類實例化優先執行。
初始化語句執行順序案例:
public class Test01 extends Father{
public static void main(String[] args) {
new Test01();
}
}
class Father{
protected static int a;//未指定值
public Father() {System.out.println("執行了父類構造器...");}
//普通代碼塊
{
System.out.println("執行了父類普通代碼塊...");
}
//靜態初始化塊
static {
System.out.println("a的值:" + a);
a = 10;
System.out.println("靜態初始化塊之後:" + a);
}
}
//a的值:0
//靜態初始化塊之後:10
//執行了父類普通代碼塊...
//執行了父類構造器...
- 案例說明:如果不創建Test01()實例,那麼只有父類靜態代碼塊和類變量被初始化。Test01實例化則會調用父類構造器,這會導致Father類的初始化。輸出結果是先執行普通代碼塊,然後執行父類構造器(普通代碼塊在實例化中執行順序最優先,調用父類構造器時不會創建父類對象)。
類初始化的時機
- 創建類的實例:1.使用new來創建實例,2.使用反射方式來創建實例,3.通過反序列化生成實例;
- 調用類方法(靜態方法);
- 訪問類變量或者接口的類變量,或者爲其賦值;
- 初始化某個類的子類,也會導致該類被初始化;
- 調用java.exe命令運行某個類,該類會被優先初始化。
初始化注意點:1.訪問常量不會導致初始化,因爲常量在編譯時就已經確定值了。但如果常量的值是運行時才被確定,就會導致類初始化;2.使用反射獲取某個類或者接口的Class實例會導致類初始化。例如Class.forName()方法對數據庫驅動類初始化。
個人理解:執行的操作需要以類存在作爲前提就會導致類初始化。例如執行對象實例化、調用類變量、反序列化等操作,如果類不存在就會出錯。
類加載器
- Bootstrap ClassLoader(原生類加載器)-----------加載路徑:JRE/lib/rt.jar
- Extension ClassLoader(擴展類加載器)-----------加載路徑:JRE/lib/ext/*.jar或者任何指向java.ext.dirs的路徑
- Application ClassLoader(應用類加載器)-----------加載路徑:classpath環境變量指定的jar或目錄。未指定classpath則使用當前類路徑。
獲取默認類加載器案例:
public static void main(String[] args) {
//獲取當前類
System.out.println("Test02的類加載器:" + Test02.class.getClassLoader());
ClassLoader cl = ClassLoader.getSystemClassLoader();
//遞歸獲取所有類加載器
while(cl != null) {
System.out.println(cl);
cl = cl.getParent();
}
}
- 案例說明:打印輸出ExtClassLoader和AppClassLoader,但沒有Bootstrap加載器,因爲Bootstrap不是Java實現的,返回null值。
加載器遵循原則:
- 父類委託:類加載器加載一個類時,會委託它的上層類加載器來對其加載,一直往上委託到Bootstrap類加載器,當所有上層加載器都不能加載該類時,纔會從自身的類路徑中去加載類文件。
- 全盤負責:某個類被類加載器加載成功後,該類中引用的其他類也會被同一個類加載器所加載。保證加載的類屬於同一個。
-
單一性:父加載器加載過的類不能被子加載器加載第二次。
URLClassLoader類
public class Test {
private static Connection con = null;
public static Connection getJDBC(String user, String pwd, String database) throws Exception {
//定位本地驅動jar包
URL[] urls = {new URL("file:///E://sqljdbc4.jar")};
//屬性類設置數據庫的賬號密碼
Properties pro = new Properties();
pro.setProperty("user", user);
pro.setProperty("password", pwd);
//創建URLClassLoader對象
URLClassLoader ucl = new URLClassLoader(urls);
//加載驅動類(初始化),之後返回驅動類Class實例
Class cla = ucl.loadClass("com.microsoft.sqlserver.jdbc.SQLServerDriver");
//把該類對象轉成Driver
Driver driver = (Driver)cla.newInstance();
//連接驅動,返回Connection對象
con = driver.connect(database, pro);
System.out.println("連接成功!");
return con;
}
public static void main(String[] args) throws Exception {
String url = "jdbc:sqlserver://localhost:1433;DatabaseName=TestDB"; //連接的數據庫
Connection con = Test.getJDBC("sa", "system", url);
}
}