java.lang包—类加载器ClassLoader类
注意:
- 類加載器的知識與JVM內存模型緊密相連,要學好這塊的知識,一定要掌握JVM的內存模型。
- 關于JVM內存模型,推薦閱讀:JVM—內存模型JMM
目錄
一、什么是ClassLoader?
二、類加載過程
三、類加載器種類及范圍
四、ClassLoader加載類的原理
一、什么是ClassLoader?
大家都知道,當我們寫好一個Java程序之后,不是管是CS還是BS應用,都是由若干個.class文件組織而成的一個完整的Java應用程序,當程序在運行時,即會調用該程序的一個入口函數來調用系統的相關功能,而這些功能都被封裝在不同的class文件當中,所以經常要從這個class文件中要調用另外一個class文件中的方法,如果另外一個文件不存在的,則會引發系統異常。而程序在啟動的時候,并不會一次性加載程序所要用的所有class文件,而是根據程序的需要,通過Java的類加載機制(ClassLoader)來動態加載某個class文件到內存當中的,從而只有class文件被載入到了內存之后,才能被其它class所引用。所以ClassLoader就是用來動態加載class文件到內存當中用的。
重點:class加載到內存指的是加載到JVM內存中,且class文件中的方法存儲在JVM的方法區。
二、類加載過程
「加載」將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然后在內存上創建一個java.lang.Class對象用來封裝類在方法區內的數據結構作為這個類的各種數據的訪問入口。
「驗證」主要是為了確保class文件中的字節流包含的信息是否符合當前JVM的要求,且不會危害JVM自身安全,比如校驗文件格式、是否是cafe baby魔術、字節碼驗證等等。
「準備」類變量(也叫靜態變量)是被static修飾的變量,準備階段為類變量分配內存并設置初始值,使用的是方法區的內存。實例變量不會在這個階段分配內存,它會在對象實例化的時候隨著對象一起分配在java堆中。比如:
//類變量age會在準備階段過后為 其分配四個(int四個字節)字節的空間,并且設置初始值為0,而不是26。若是final的,則在編譯期就會設置上最終值。 private?static?int?age?=?26;「解析」JVM會在此階段把類的二進制數據中的符號引用替換為直接引用。
「初始化」初始化階段即虛擬機執行類構造器clinit() 方法的過程,對類的靜態方法、靜態變量和靜態代碼塊初始化的過程。到了初始化階段,才真正開始執行類定義的Java程序代碼(或者說字節碼 )。比如準備階段的那個age初始值是0,到這一步就設置為26。
「使用」對象都出來了,業務系統直接調用階段。
「卸載」用完了,可以被GC回收了。
類加載過程最重要的3個階段是:加載、準備、初始化,簡單總結這三個過程就是如下這個過程:
- 加載:類文件加載到JVM方法區中
- 準備:在JVM方法區中為靜態變量開辟內存地址,初始值全部為0或null
- 初始化:為靜態變量賦值,執行靜態方法和靜態代碼塊的代碼
三、類加載器種類及范圍
1、BootStrap ClassLoader:
最頂層類加載器,他的父類加載器是個null,也就是沒有父類加載器。負責加載jvm的核心類庫,比如java.lang.*等,從系統屬性中的sun.boot.class.path所指定的目錄中加載類庫。他的具體實現由Java虛擬機底層C++代碼實現。
System.out.println(System.getProperty("sun.boot.class.path"));打印結果:
- C:\Program Files\Java\jdk1.6.0_22\jre\lib\resources.jar;
- C:\Program Files\Java\jdk1.6.0_22\jre\lib\rt.jar;
- C:\Program Files\Java\jdk1.6.0_22\jre\lib\sunrsasign.jar;
- C:\Program Files\Java\jdk1.6.0_22\jre\lib\jsse.jar;
- C:\Program Files\Java\jdk1.6.0_22\jre\lib\jce.jar;C:\Program Files\Java\jdk1.6.0_22\jre\lib\charsets.jar;
- C:\Program Files\Java\jdk1.6.0_22\jre\classes
- URL[]?urls?=?sun.misc.Launcher.getBootstrapClassPath().getURLs();??
2、Extension ClassLoader:
父類加載器是Bootstrap ClassLoader。從java.ext.dirs系統屬性所指定的目錄中加載類庫,或者從JDK的安裝目錄的JAVA_HOME/jre/lib/ext/子目錄(擴展目錄)下加載類庫,如果把用戶的jar文件放在這個目錄下,也會自動由擴展類加載器加載。繼承自java.lang.ClassLoader。
3、App ClassLoader:
父類加載器是Extension ClassLoader。從環境變量classpath或者系統屬性java.class.path所指定的目錄中加載類。繼承自java.lang.ClassLoader。
4、自定義類加載器(User ClassLoader)
除了上面三個自帶的以外,用戶還能制定自己的類加載器,但是所有自定義的類加載器都應該繼承自java.lang.ClassLoader。比如熱部署、tomcat都會用到自定義類加載器。
// sun.misc.Launcherpublic class Launcher {// Bootstrap類加載器的加載路徑,在static靜態代碼塊里用的private static String bootClassPath = System.getProperty("sun.boot.class.path");// AppClassLoader 繼承 ClassLoaderstatic class AppClassLoader extends URLClassLoader {public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {// java.class.pathfinal String var1 = System.getProperty("java.class.path");}}// ExtClassLoader 繼承 ClassLoaderstatic class ExtClassLoader extends URLClassLoader {public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {// java.ext.dirsString var0 = System.getProperty("java.ext.dirs");}} }四、ClassLoader加載類的原理
1、原理介紹
ClassLoader使用的是雙親委托模型來搜索類的,每個ClassLoader實例都有一個父類加載器的引用(不是繼承的關系,是一個包含的關系),虛擬機內置的類加載器(Bootstrap ClassLoader)本身沒有父類加載器,但可以用作其它ClassLoader實例的的父類加載器。當一個ClassLoader實例需要加載某個類時,他首先會從自己緩存里查找是否之前加載過這個class,加載過直接返回,沒加載過的話他不會自己親自去加載,他會把這個請求委派給父類加載器去完成,這個過程是由上至下依次檢查的,首先由最頂層的類加載器Bootstrap ClassLoader試圖加載,如果沒加載到,則把任務轉交給Extension ClassLoader試圖加載,如果也沒加載到,則轉交給App ClassLoader?進行加載,如果它也沒有加載得到的話,則返回給委托的發起者,由它到指定的文件系統或網絡等URL中加載該類。如果它們都沒有加載到這個類時,則拋出ClassNotFoundException異常。否則將這個找到的類生成一個類的定義,并將它加載到內存當中,最后返回這個類在內存中的Class實例對象。如下圖所示,紅色框表示各個類加載器:
正向調用過程: .class文件先申請Custom加載器加載,Custom加載器緩存中有的話直接返回,沒有則調用App加載器進行加載;App加載器緩存中有的話直接返回,沒有則調用Ext加載器;Ext加載器緩存中有的話直接返回,沒有則調用Bootstrap加載器。
反向調用過程:Bootstrap加載器緩存中有的話直接返回,沒有則回調Ext加載器進行加載;Ext進行加載,加載到返回成功,加載不到回調App加載器;App進行加載,加載到返回成功,加載不到回調Custom加載器;Custom進行加載,加載到返回成功,加載不到返回失敗;
雙親委派模型源碼在java.lang.ClassLoader#loadClass(java.lang.String, boolean)
3、為啥要有雙親委派
防止內存中出現多份同樣的字節碼,安全。比如自己重寫個java.lang.Object并放到Classpath中,沒有雙親委派的話直接自己執行了,那不安全。雙親委派可以保證這個類只能被頂層Bootstrap Classloader類加載器加載,從而確保只有JVM中有且僅有一份正常的java核心類。如果有多個的話,那么就亂套了。比如相同的類instance of可能返回false,因為可能父類不是同一個類加載器加載的Object。
4、為什么需要破壞雙親委派模型
1)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類加載器去進行加載了。
2)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開始就是其中之一。
- 熱部署的場景會破壞,否則實現不了熱部署。
5、如何破壞雙親委派模型
重寫loadClass方法,別重寫findClass方法,因為loadClass是核心入口,將其重寫成自定義邏輯即可破壞雙親委派模型。
6、如何自定義一個類加載器
只需要繼承java.lang.Classloader類,然后覆蓋他的findClass(String name)方法即可,該方法根據參數指定的類名稱,返回對應 的Class對象的引用。
7、熱部署原理
采取破壞雙親委派模型的手段來實現熱部署,默認的loadClass()方法先找緩存,你改了class字節碼也不會熱加載,所以自定義ClassLoader,去掉找緩存那部分,直接就去加載,也就是每次都重新加載。
五、常見筆試題
1)以下代碼的輸出結果是什么?
public class Test1 {static {// 編譯沒報錯i = 2;// 編譯報錯Illegal forward referenceSystem.out.println(i);}private static int i =1;public static void main(String[] args) {} }答案:編譯報錯,如下圖所示(IDea會直接提示報錯,想編譯的話在命令行中編譯吧)
代碼分析:準備階段為變量i在JVM分配內存地址。初始化階段為變量賦值,在這段代碼中存在兩個賦值動作,一個是int i =1 ??還有一個是i = 2 ,這兩個動作都是在類加載的初始化階段執行的,那么i最后的值到底是1還是2呢?這個是不好確定的,所以JVM不允許前向引用(這個其實就是c語言里面的不允許前向引用,畢竟JVM底層也是c寫的)。去掉語句System.out.println(i);編譯通過
?
2):輸出結果是什么?
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) {// 1System.out.println(test2.value1);// 3System.out.println(test2.value2);} }答案 :1、3。因為類加載的準備過程中會先準備類變量(也就是靜態變量),此時test2=null,value1=0,value2=0,然后進入初始化階段,先執行test2=new Test2(),會執行構造器,結果是value1 = 1,value2 = 1,然后執行value1和value2這兩句,value1沒變化,value2被重新賦值成了3,所以結果1和3。
3)如果把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) {// 1System.out.println(test3.value1);// 4System.out.println(test3.value2);} }讀后有收獲可以支付寶請作者喝奶茶?
?
總結
以上是生活随笔為你收集整理的java.lang包—类加载器ClassLoader类的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java.lang包—类Class
- 下一篇: java.lang包—对象基类Objec