1、類加載程序

類加載時機
「加載」
將類的.class檔案中的二進制資料讀入到記憶體中,將其放在運行時資料區的方法區內,然后在記憶體上創建一個java.lang.Class物件用來封裝類在方法區內的資料結構作為這個類的各種資料的訪問入口,
「驗證」
主要是為了確保class檔案中的位元組流包含的資訊是否符合當前JVM的要求,且不會危害JVM自身安全,比如校驗檔案格式、是否是cafe baby魔術、位元組碼驗證等等,
「準備」
為類變數分配記憶體并設定類變數(是被static修飾的變數,變數不是常量,所以不是final的,就是static的)初始值的階段,這些變數所使用的記憶體在方法區中進行分配,比如
private static int age = 26;
類變數age會在準備階段過后為 其分配四個(int四個位元組)位元組的空間,并且設定初始值為0,而不是26,
若是final的,則在編譯期就會設定上最終值,
「決議」
JVM會在此階段把類的二進制資料中的符號參考替換為直接參考,
「初始化」
初始化階段是執行類構造器<clinit>()方法的程序,到了初始化階段,才真正開始執行類定義的Java程式代碼(或者說位元組碼 ),比如準備階段的那個age初始值是0,到這一步就設定為26,
「使用」
物件都出來了,業務系統直接呼叫階段,
「卸載」
用完了,可以被GC回收了,
2、類加載器種類以及加載范圍

類加載器種類
「啟動類加載器(Bootstrap ClassLoader)」
最頂層類加載器,他的父類加載器是個null,也就是沒有父類加載器,負責加載jvm的核心類別庫,比如java.lang.*等,從系統屬性中的sun.boot.class.path所指定的目錄中加載類別庫,他的具體實作由Java虛擬機底層C++代碼實作,
「擴展類加載器(Extension ClassLoader)」
父類加載器是Bootstrap ClassLoader,從java.ext.dirs系統屬性所指定的目錄中加載類別庫,或者從JDK的安裝目錄的jre/lib/ext子目錄(擴展目錄)下加載類別庫,如果把用戶的jar檔案放在這個目錄下,也會自動由擴展類加載器加載,繼承自java.lang.ClassLoader,
「應用程式類加載器(Application ClassLoader)」
父類加載器是Extension ClassLoader,從環境變數classpath或者系統屬性java.class.path所指定的目錄中加載類,繼承自java.lang.ClassLoader,
「自定義類加載器(User ClassLoader)」
除了上面三個自帶的以外,用戶還能制定自己的類加載器,但是所有自定義的類加載器都應該繼承自java.lang.ClassLoader,比如熱部署、tomcat都會用到自定義類加載器,
補充:不同ClassLoader加載的檔案路徑配置在如下原始碼里寫的:
// sun.misc.Launcher public class Launcher { // Bootstrap類加載器的加載路徑,在static靜態代碼塊里用的 private static String bootClassPath = System.getProperty("sun.boot.class.path"); // AppClassLoader 繼承 ClassLoader static class AppClassLoader extends URLClassLoader { public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException { // java.class.path final String var1 = System.getProperty("java.class.path"); } } // ExtClassLoader 繼承 ClassLoader static class ExtClassLoader extends URLClassLoader { public static Launcher.ExtClassLoader getExtClassLoader() throws IOException { // java.ext.dirs String var0 = System.getProperty("java.ext.dirs"); } } }
3、雙親委派是什么
如果一個類加載器收到了類加載的請求,他首先會從自己快取里查找是否之前加載過這個class,加載過直接回傳,沒加載過的話他不會自己親自去加載,他會把這個請求委派給父類加載器去完成,每一層都是如此,類似遞回,一直遞回到頂層父類,
也就是Bootstrap ClassLoader,只要加載完成就會回傳結果,如果頂層父類加載器無法加載此class,則會回傳去交給子類加載器去嘗試加載,若最底層的子類加載器也沒找到,則會拋出ClassNotFoundException,
原始碼在java.lang.ClassLoader#loadClass(java.lang.String, boolean)

雙親委派模型
4、為啥要有雙親委派
防止記憶體中出現多份同樣的位元組碼,安全,
比如自己重寫個java.lang.Object并放到Classpath中,沒有雙親委派的話直接自己執行了,那不安全,雙親委派可以保證這個類只能被頂層Bootstrap Classloader類加載器加載,從而確保只有JVM中有且僅有一份正常的java核心類,如果有多個的話,那么就亂套了,比如相同的類instance of可能回傳false,因為可能父類不是同一個類加載器加載的Object,
5、為什么需要破壞雙親委派模型
Jdbc
Jdbc為什么要破壞雙親委派模型?
以前的用法是未破壞雙親委派模型的,比如Class.forName("com.mysql.cj.jdbc.Driver");
而在JDBC4.0以后,開始支持使用spi的方式來注冊這個Driver,具體做法就是在mysql的jar包中的META-INF/services/java.sql.Driver檔案中指明當前使用的Driver是哪個,然后使用的時候就不需要我們手動的去加載驅動了,我們只需要直接獲取連接就可以了,Connection con = DriverManager.getConnection(url, username, password );
首先,理解一下為什么JDBC需要破壞雙親委派模式,原因是原生的JDBC中Driver驅動本身只是一個介面,并沒有具體的實作,具體的實作是由不同資料庫型別去實作的,例如,MySQL的mysql-connector-*.jar中的Driver類具體實作的,
原生的JDBC中的類是放在rt.jar包的,是由Bootstrap加載器進行類加載的,在JDBC中的Driver類中需要動態去加載不同資料庫型別的Driver類,而mysql-connector-*.jar中的Driver類是用戶自己寫的代碼,那Bootstrap類加載器肯定是不能進行加載的,既然是自己撰寫的代碼,那就需要由Application類加載器去進行類加載,
這個時候就引入執行緒背景關系件類加載器(Thread Context ClassLoader),通過這個東西程式就可以把原本需要由Bootstrap類加載器進行加載的類由Application類加載器去進行加載了,
Tomcat
Tomcat為什么要破壞雙親委派模型?
因為一個Tomcat可以部署N個web應用,但是每個web應用都有自己的classloader,互不干擾,比如web1里面有com.test.A.class,web2里面也有com.test.A.class,如果沒打破雙親委派模型的話,那么web1加載完后,web2在加載的話會沖突,
因為只有一套classloader,卻出現了兩個重復的類路徑,所以tomcat打破了,他是執行緒級別的,不同web應用是不同的classloader,
-
Java spi 方式,比如jdbc4.0開始就是其中之一,
-
熱部署的場景會破壞,否則實作不了熱部署,
6、如何破壞雙親委派模型
重寫loadClass方法,別重寫findClass方法,因為loadClass是核心入口,將其重寫成自定義邏輯即可破壞雙親委派模型,
7、如何自定義一個類加載器
只需要繼承java.lang.Classloader類,然后覆寫他的findClass(String name)方法即可,該方法根據引數指定的類名稱,回傳對應 的Class物件的參考,
8、熱部署原理
采取破壞雙親委派模型的手段來實作熱部署,默認的loadClass()方法先找快取,你改了class位元組碼也不會熱加載,所以自定義ClassLoader,去掉找快取那部分,直接就去加載,也就是每次都重新加載,
9、常見筆試題
問題:輸出結果是什么?
答案:編譯報錯,
原因:因為靜態陳述句塊中只能訪問定義在靜態陳述句塊之前的變數,定義在他之后的 變數在前面的靜態陳述句塊中可以賦值,但是不能訪問,
/** * Description: 編譯報錯 * * @author TongWei.Chen 2021-01-08 17:37:44 */ public class Test1 { static { // 編譯沒報錯 i = 2; // 編譯報錯Illegal forward reference System.out.println(i); } private static int i =1; }
問題:輸出結果是什么?
答案 :1、3
原因:因為類加載程序中會先準備類變數(也就是靜態變數),準備階段是賦初始值階段,也就是test2=null,value1=0,value2=0,然后進入初始化階段的時候test2=new Test2(),會執行構造器,結果是value1 = 1,value2 = 4,然后執行value1和value2這兩句,value1沒變化,value2被重新賦值成了3,所以結果1和3,
public class Test2 { private static Test2 test2 = new Test2(); private static int value1; private static int value2 = 3; private Test2() { value1 ++; value2 ++; } public static void main(String[] args) { // 1 System.out.println(test2.value1); // 3 System.out.println(test2.value2); } }
那如果把private static Test2 test2 = new Test2();放到private static int value2 = 3;下面的話結果就是1和4了,
public class Test3 { private static int value1; private static int value2 = 3; private static Test3 test3 = new Test3(); private Test3() { value1 ++; value2 ++; } public static void main(String[] args) { // 1 System.out.println(test3.value1); // 4 System.out.println(test3.value2); } }
END
推薦好文
強大,10k+點贊的 SpringBoot 后臺管理系統竟然出了詳細教程!
分享一套基于SpringBoot和Vue的企業級中后臺開源專案,代碼很規范!
能掙錢的,開源 SpringBoot 商城系統,功能超全,超漂亮!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/247926.html
標籤:Java
