Day18 (一)類的加載器
一個運行時的Java虛擬機(JVM)負責運行一個Java程序。
當啟動一個Java程序時,一個虛擬機實例誕生;當程序關閉退出,這個虛擬機實例也就隨之消亡。
如果在同一臺計算機上同時運行多個Java程序,將得到多個Java虛擬機實例,每個Java程序都運行於它自己的Java虛擬機實例中。
在如下幾種情況下,Java虛擬機將結束生命周期:
1.執行了System.exit()方法
2.程序正常執行結束
3.程序在執行過程中遇到了異常或錯誤而異常終止
4.由於操作系統出現錯誤而導致Java虛擬機進程終止
類的生命周期
類的加載
xx.class的字節碼文件,把字節碼文件加載到方法區中
類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然後在堆區創建一個java.lang.Class對象,用來封裝類在方法區內的數據結構。
加載.class文件的方式
1.從本地系統中直接加載
2.通過網絡下載.class文件
3.從zip,jar等歸檔文件中加載.class文件
4.從專有數據庫中提取.class文件
5.將Java源文件動態編譯為.class文件
類的加載的最終產品是位於堆區中的Class對象。
Class對象封裝了類在方法區內的數據結構,並且向Java程序員提供了訪問方法區內的數據結構的接口。
驗證:驗證字節碼文件的準確性。
類的驗證內容:
1.類文件的結構檢查
確保類文件遵從Java類文件的固定格式。
2.語義檢查
確保類本身符合Java語言的語法規定,比如驗證final類型的類沒有子類,以及final類型的方法沒有被覆蓋。
註意,語義檢查的錯誤在編譯器編譯階段就會通不過,但是如果有程序員通過非編譯的手段生成了類文件,其中有可能會含有語義錯誤,此時的語義檢查主要是防止這種沒有編譯而生成的class文件引入的錯誤。
3.字節碼驗證
確保字節碼流可以被Java虛擬機安全地執行。
字節碼流代表Java方法(包括靜態方法和實例方法),它是由被稱作操作碼的單字節指令組成的序列,每一個操作碼後都跟著一個或多個操作數。
字節碼驗證步驟會檢查每個操作碼是否合法,即是否有著合法的操作數。
4.二級制兼容性的驗證
確保相互引用的類之間的協調一致。
例如,在Worker類的gotoWork()方法中會調用Car類的run()方法,Java虛擬機在驗證Worker類時,會檢查在方法區內是否存在Car類的run()方法,假如不存在(當Worker類和Car類的版本不兼容就會出現這種問題),就會拋出NoSuchMethodError錯誤。
準備:給類變量(靜態變量)分配空間並且進行默認初始化。
在準備階段,Java虛擬機為類的靜態變量分配內存,並設置默認的初始值。
例如對於以下Sample類,在準備階段,將為int類型的靜態變量a分配4個字節的內存空間,並且賦予默認值0,為long類型的靜態變量b分配8個字節的內存空間,並且賦予默認值0。
public class Sample { private static int a = 1; private static long b; static { b = 2; } }
解析:把字節碼中的一些符號轉換成指針
在解析階段,Java虛擬機會把類的二級制數據中的符號引用替換為直接引用。
例如在Worker類的gotoWork()方法中會引用Car類的run()方法。
public void gotoWork() { car.run();// 這段代碼在Worker類的二進制數據中表示為符號引用 }
在Worker類的二進制數據中,包含了一個對Car類的run()方法的符號引用,它由run()方法的全名和相關描述符組成。
在解析階段,Java虛擬機會把這個符號引用替換為一個指針,該指針指向Car類的run()方法在方法區內的內存位置,這個指針就是直接引用。
類變量(靜態變量)進行聲明處或靜態塊處初始化。
類的初始化步驟
1.假如這個類還沒有被加載和連接,那就先進行加載和連接。
2.假如類存在直接的父類,並且這個父類還沒有被初始化,那就先初始化直接的父類。
3.假如類中存在初始化語句,那就依次執行這些初始化語句。
類的初始化時機
Java程序對類的使用方式可以分為兩種:
1.主動使用
2.被動使用
所有的Java虛擬機實現必須在每個類或接口被Java程序首次主動使用時才初始化它們。
主動使用的六種情況:
1.創建類的實例。
2.訪問某個類或接口的靜態變量,或者對該靜態變量賦值。
3.調用類的靜態方法
4.當使用反射方法強制創建某個類或接口的對象時
5.當初始化某個子類時,該子類的所有父類都會被初始化
6.當虛擬機Java命令運行啟動類
除了以上六種情況,其他使用Java類的方式都被看作是對類的被動使用,都不會導致類的初始化。
調用類中的方法
卸載
類的加載器
Bootstrap、ClassLoader
負責加載核心類庫 lib下(C:\Program Files\Java\jdk1.8.0_151\jre\lib),是用原生代碼來實現的(C實現的),並不繼承自java.lang.ClassLoader。
加載擴展類和應用程序類加載器,並指定它們的父類加載器。
Extension ClassLoader,父類是根類加載器
用來加載java的擴展庫(JAVA_HOME/jre/lib/ext/*.jar,或java.ext.dirs路徑下的內容)java虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄裏面查找並加載java類。
由sun.miscLauncher$ExtClassLoader實現,繼承自java.lang.ClassLoader
System ClassLoader,父類是擴展類加載器
負責加載自己編寫的類 classpath下
自定義類加載器
public static void main(String[] args) { //AppClassLoader 系統類加載器 System.out.println(TestLoader1.class.getClassLoader()); //ExtClassLoader 擴張類加載器 System.out.println(TestLoader1.class.getClassLoader().getParent()); //null根類加載器無法查看 System.out.println(TestLoader1.class.getClassLoader().getParent().getParent()); }
類加載的父委托機制
loader2首先從自己的命名空間中查找Sample類是否已經被加載,如果已經加載,就直接返回代表Sample類的Class對象的引用。
如果Sample類還沒有被加載,loader2首先請求loader1代為加載,loader1再請求系統類加載器代為加載,系統類加載器再請求擴展類加載器代為加載,擴展類加載器再請求根類加載器代為加載。
若根類加載器和擴展類加載器都不能加載,則系統類加載器嘗試加載,若能加載成功,則將Sample類所對應的Class對象的引用返回給loader1,loader1再返回給loader2,從而成功將Sample類加載進虛擬機。
若系統加載器不能加載Sample類,則loader1嘗試加載Sample類,若loader1也不能成功加載,則loader2嘗試加載。
若所有的父加載器及loader2本身都不能加載,則拋出ClassNotFoundException異常。
總結下來就是:
每個加載器都優先嘗試用父類加載,若父類不能加載則自己嘗試加載;若成功則返回Class對象給子類,若失敗則告訴子類讓子類自己加載。所有都失敗則拋出異常。
類加載器的工作原理
類加載器的工作原理基於三個機制:委托、可見性和單一性。
委托機制
當一個類加載和初始化的時候,類僅在有需要加載的時候被加載。假設你有一個應用需要的類叫作Abc.class,首先加載這個類的請求由Application類加載器委托給它的父類加載器Extension類加載器,然後再委托給Bootstrap類加載器。Bootstrap類加載器會先看看rt.jar中有沒有這個類,因為並沒有這個類,所以這個請求由回到Extension類加載器,它會查看jre/lib/ext目錄下有沒有這個類,如果這個類被Extension類加載器找到了,那麽它將被加載,而Application類加載器不會加載這個類;而如果這個類沒有被Extension類加載器找到,那麽再由Application類加載器從classpath中尋找。記住classpath定義的是類文件的加載目錄,而PATH是定義的是可執行程序如javac,java等的執行路徑。
可見性機制
根據可見性機制,子類加載器可以看到父類加載器加載的類,而反之則不行。所以下面的例子中,當Abc.class已經被Application類加載器加載過了,然後如果想要使用Extension類加載器加載這個類,將會拋出java.lang.ClassNotFoundException異常。
package test; import java.util.logging.Level; import java.util.logging.Logger; /** * Java program to demonstrate How ClassLoader works in Java, * in particular about visibility principle of ClassLoader. * * @author Javin Paul */ public class ClassLoaderTest { public static void main(String args[]) { try { //printing ClassLoader of this class System.out.println("ClassLoaderTest.getClass().getClassLoader() : " + ClassLoaderTest.class.getClassLoader()); //trying to explicitly load this class again using Extension class loader Class.forName("test.ClassLoaderTest", true , ClassLoaderTest.class.getClassLoader().getParent()); } catch (ClassNotFoundException ex) { Logger.getLogger(ClassLoaderTest.class.getName()).log(Level.SEVERE, null, ex); } } }
輸出
ClassLoaderTest.getClass().getClassLoader() : sun.misc.Launcher$AppClassLoader@601bb1 16/08/2012 2:43:48 AM test.ClassLoaderTest main SEVERE: null java.lang.ClassNotFoundException: test.ClassLoaderTest at java.net.URLClassLoader$1.run(URLClassLoader.java:202) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:190) at sun.misc.Launcher$ExtClassLoader.findClass(Launcher.java:229) at java.lang.ClassLoader.loadClass(ClassLoader.java:306) at java.lang.ClassLoader.loadClass(ClassLoader.java:247) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:247) at test.ClassLoaderTest.main(ClassLoaderTest.java:29)
單一性機制
根據這個機制,父加載器加載過的類不能被子加載器加載第二次。雖然重寫違反委托和單一性機制的類加載器是可能的,但這樣做並不可取。你寫自己的類加載器的時候應該嚴格遵守這三條機制。
如何顯式的加載類
Java提供了顯式加載類的API:Class.forName(classname)和Class.forName(classname, initialized, classloader)。就像上面的例子中,你可以指定類加載器的名稱以及要加載的類的名稱。類的加載是通過調用java.lang.ClassLoader的loadClass()方法,而loadClass()方法則調用了findClass()方法來定位相應類的字節碼。在這個例子中Extension類加載器使用了java.net.URLClassLoader,它從JAR和目錄中進行查找類文件,所有以”/”結尾的查找路徑被認為是目錄。如果findClass()沒有找到那麽它會拋出java.lang.ClassNotFoundException異常,而如果找到的話則會調用defineClass()將字節碼轉化成類實例,然後返回。
什麽地方使用類加載器
類加載器是個很強大的概念,很多地方被運用。最經典的例子就是AppletClassLoader,它被用來加載Applet使用的類,而Applets大部分是在網上使用,而非本地的操作系統使用。使用不同的類加載器,你可以從不同的源地址加載同一個類,它們被視為不同的類。J2EE使用多個類加載器加載不同地方的類,例如WAR文件由Web-app類加載器加載,而EJB-JAR中的類由另外的類加載器加載。有些服務器也支持熱部署,這也由類加載器實現。你也可以使用類加載器來加載數據庫或者其他持久層的數據。
Day18 (一)類的加載器