萬萬沒想到,面試中,連 ClassLoader類載入器 也能問出這麼多問題…..
1、類載入過程
類載入時機
「載入」
將類的.class檔案中的二進位制資料讀入到記憶體中,將其放在執行時資料區的方法區內,然後在記憶體上建立一個java.lang.Class
物件用來封裝類在方法區內的資料結構作為這個類的各種資料的訪問入口。
「驗證」
主要是為了確保class檔案中的位元組流包含的資訊是否符合當前JVM的要求,且不會危害JVM自身安全,比如校驗檔案格式、是否是cafe baby魔術、位元組碼驗證等等。
「準備」
為類變數分配記憶體並設定類變數(是被static修飾的變數,變數不是常量,所以不是final的,就是static的)初始值的階段。這些變數所使用的記憶體在方法區中進行分配。比如
privatestaticintage=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 後臺管理系統竟然出了詳細教程!