1、什么是類的加載
類的加載指的是將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,然后在java堆區創建一個java.lang.Class對象,用來封裝類在方法區內的數據結構。類的加載的最終產品是位於堆區中的Class 對象。Class對象封裝了類在方法區內的數據結構,並且向Java程序員提供了訪問方法區內的數據結構的接口。
2、 類的加載過程
JVM 將類的加載過程分為三個大的步驟:加載(loading),鏈接(link),初始化(initialize)。其中鏈接又分為三個步驟:驗證,准備,解析。
(1) 加載:查找並加載類的二進制數據
加載是類加載過程中的第一個階段,加載過程虛擬機需要完成以下三件事情:
1) 通過一個類的全限定名來獲取其定義的二進制字節流;
2) 將這個字節流所代表的靜態存儲結構轉為方法區的運行時數據結構;
3) 在Java 堆中生成一個代表這個類的java.lang.Class 對象,作為方法區中這些數據的訪問入口。
相對於類加載的其他階段而言,加載階段(准確地說,是加載階段獲取類的二進制字節流的動作)是可控性最強的階段,因為開發人員既可以使用系統提供的類加載器來完成加載,也可以自定義自己的類加載器來完成加載。
加載階段完成后,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中,而且在Java堆中也創建一個java.lang.Class類的對象,這樣便可以通過該對象訪問方法區中的這些數據
(2) 鏈接:
① 驗證:確保被加載類的正確性;
主要是為了安全考慮,為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。
② 准備:為類的靜態變量分配內存,並將其初始化為默認值;
准備階段是正式為類變量分配內存並設置類變量初始值的階段,這些內存都將在方法區中分配。對於該階段有以下幾點需要注意:
1)、這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨着對象一塊分配在Java堆中。
2)、這里所設置的初始值通常情況下是數據類型默認的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值。
假設一個類變量的定義為:public static int value = 3;
那么變量value在准備階段過后的初始值為0,而不是3,因為這時候尚未開始執行任何Java方法,而把value賦值為3的putstatic指令是在程序編譯后,存放於類構造器<clinit>()方法之中的,所以把value賦值為3的動作將在初始化階段才會執行。
③ 解析:把類中的符號引用轉換為直接引用;
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程,解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。
(3) 初始化:為類的靜態變量賦予正確的初始值
為類的靜態變量賦予正確的初始值,JVM負責對類進行初始化,主要對類變量進行初始化。在Java中對類變量進行初始值設定有兩種方式:
① 聲明類變量是指定初始值;
② 使用靜態代碼塊為類變量指定初始值;
③ JVM初始化步驟
1)、假如這個類還沒有被加載和連接,則程序先加載並連接該類
2)、假如該類的直接父類還沒有被初始化,則先初始化其直接父類
3)、假如類中有初始化語句,則系統依次執行這些初始化語句
④ 類初始化時機:只有當對類的主動使用的時候才會導致類的初始化,類的主動使用包括以下六種:
– 創建類的實例,也就是new的方式
– 訪問某個類或接口的靜態變量,或者對該靜態變量賦值
– 調用類的靜態方法
– 反射(如Class.forName(“com.shengsiyuan.Test”))
– 初始化某個類的子類,則其父類也會被初始化
– Java虛擬機啟動時被標明為啟動類的類(Java Test),直接使用java.exe命令來運行某個主類
(4) 結束生命周期
在如下幾種情況下,Java虛擬機將結束生命周期
– 執行了System.exit()方法
– 程序正常執行結束
– 程序在執行過程中遇到了異常或錯誤而異常終止
– 由於操作系統出現錯誤而導致Java虛擬機進程終止
3、類加載器
JVM 類加載器作用,將class文件字節碼內容加載到內存中,並將這些靜態數據轉換成方法區中的運行時數據結構,在堆中生成一個代表這個類的java.lang.Class對象,作為方法區類數據的訪問入口。
類加載器是通過ClassLoader 及其子類來完成的,類的層次關系和加載順序可以由下圖來描述:
(1) Bootstrap ClassLoader 引導類加載器
負責加載Java核心庫$JAVA_HOME中的jre/lib/rt.jar 里所有的class,由c++實現,不是ClassLoader子類。
(2) Extension ClassLoader 擴展類加載器
負責加載Java 平台中擴展功能的一些jar包,包括$JAVA_HOME中的jre/lib/ext/*.jar 或 -D java.ext.dirs指定目錄下的jar包。
(3) App ClassLoader
負責加載classpath 中指定的jar包及目錄中class
(4) Custom ClassLoader
應用程序根據自身需要自定義的ClassLoader,如tomcat,jboss 都會根據j2ee規范自行實現ClassLoader,加載過程中會先檢查是否已被加載,檢查順序是自底向上,從Custom ClassLoader 到BootStrap ClassLoader逐層檢查,只要某個classloader已加載就視為已加載此類,保證此類在所有ClassLoader 只加載一次。而加載的順序是自頂向下,也就是由上層來逐層嘗試加載此類。
4、JVM 三種預定義加載器
JVM預定義有三種類加載器,當一個 JVM啟動的時候,Java 默認開始使用如下三種類加載器:
(1) 引導類加載器(Bootstrap class loader):它用來加載 Java 的核心庫,是用原生代碼來實現的,並不繼承自 java.lang.ClassLoader。它負責將<Java_Runtime_Home>/lib下面的核心類庫或-Xbootclasspath選項指定的jar包加載到內存中。由於引導類加載器涉及到虛擬機本地實現細節,開發者無法直接獲取到啟動類加載器的引用,所以不允許直接通過引用進行操作。
(2) 擴展類加載器(Extensions class loader):該類加載器在此目錄里面查找並加載 Java 類。擴展類加載器是由Sun的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它用來加載 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄。它負責將< Java_Runtime_Home >/lib/ext或者由系統變量-Djava.ext.dirs指定位置中的類庫加載到內存中。開發者可以直接使用標准擴展類加載器。
(3) 系統類加載器(System class loader):系統類加載器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。它負責將系統類路徑java -classpath或-Djava.class.path變量所指的目錄下的類庫加載到內存中。開發者可以直接使用系統類加載器。一般來說,Java 應用的類都是由它來完成加載的。可以通過 ClassLoader.getSystemClassLoader()來獲取它。
5、類加載器 "雙親委派" 機制
(1) 雙親委派機制介紹
在這里需要着重說明,JVM在加載類時默認采用的是雙親委派機制。所謂的雙親委派機制,就是某個特定的類加載器在接到類的請求時,首先將加載任務委托給父加載器,依次遞歸,如果父加載器可以完成類加載任務,就成功返回;只有父加載器無法完成此加載任務時,才自己去加載。關於虛擬機默認的雙親委派機制,我們可以從系統類加載器和標准擴展類加載器為例作簡單分析。
雙親委派機制是為了保證Java核心庫的類型安全。這種機制能保證不會出現用戶自己能定義java.lang.Object類的情況,因為即使定義了,也加載不了。
圖一 標准擴展類加載器繼承層次圖 圖二 系統類加載器繼承層次圖
圖一與圖二可以看出,類加載器均是繼承自java.lang.ClassLoader 抽象類。我們來看看java.lang.ClassLoader 中幾個最重要的方法:
//加載指定名稱(包括包名)的二進制類型,供用戶調用的接口 public Class<?> loadClass(String name) throws ClassNotFoundException{//…} //加載指定名稱(包括包名)的二進制類型,同時指定是否解析(但是,這里的resolve參數不一定真正能達到解析的效果~_~),供繼承用 protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{//…} //findClass方法一般被loadClass方法調用去加載指定名稱類,供繼承用 protected Class<?> findClass(String name) throws ClassNotFoundException {//…} //定義類型,一般在findClass方法中讀取到對應字節碼后調用,可以看出不可繼承(說明:JVM已經實現了對應的具體功能,解析對應的字節碼,產生對應的內部數據結構放置到方法區,所以 無需覆寫,直接調用就可以了) protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{//…}
通過進一步分析標准擴展類加載器(sun.misc.Launcher$ExtClassLoader) 和系統類加載器(sun.misc.Launcher$AppClassLoader)的代碼以及其公共父類(java.net.URLClassLoader和 java.security.SecureClassLoader) 的代碼可以看出,都沒有覆寫java.lang.ClassLoader中默認的加載委派規則 loadClass() 方法。既然這樣我們可以通過分析java.lang.ClassLoader中的 loadClass(String name) 方法代碼看到虛擬機默認采用的雙親委派機制到底是什么模樣:
1 public Class<?> loadClass(String name) throws ClassNotFoundException { 2 return loadClass(name, false); 3 } 4 5 protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { 6 // 首先判斷該類型是否已經被加載 7 Class c = findLoadedClass(name); 8 if (c == null) { 9 //如果沒有被加載,就委托給父類加載或者委派給啟動類加載器加載 10 try { 11 if (parent != null) { 12 //如果存在父類加載器,就委派給父類加載器加載 13 c = parent.loadClass(name, false); 14 } else { 15 //如果不存在父類加載器,就檢查是否是由啟動類加載器加載的類,通過調用本地方法native Class findBootstrapClass(String name) 16 c = findBootstrapClass0(name); 17 } 18 }catch (ClassNotFoundException e) { 19 // 如果父類加載器和啟動類加載器都不能完成加載任務,才調用自身的加載功能 20 c = findClass(name); 21 } 22 } 23 if (resolve) { 24 resolveClass(c); 25 } 26 return c; 27 }
通過上面的代碼分析,我們可以對JVM采用的雙親委派類加載機制有了更感性的認識。
我們就接着分析一下啟動類加載器、標准擴展類加載器和系統類加載器三者之間的關系。可能大家已經從各種資料上面看到了如下類似的一幅圖片:
圖三 類加載器默認委派關系圖
上面圖片給人的直觀印象是系統類加載器的父類加載器是標准擴展類加載器,標准擴展類加載器的父類加載器是啟動類加載器,下面我們就用代碼具體測試一下:
public static void main(String[] args) { System.out.println(ClassLoader.getSystemClassLoader()); System.out.println(ClassLoader.getSystemClassLoader().getParent()); System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent()); } /* 輸出結果: sun.misc.Launcher$AppClassLoader@73d16e93 sun.misc.Launcher$ExtClassLoader@15db9742 null */
通過以上的代碼輸出,我們可以判定系統類加載器的父加載器是標准擴展類加載器,但是我們試圖獲取標准擴展類加載器的父類加載器時確得到了null,就是說標准擴展類加載器本身強制設定父類加載器為null。我們借助於代碼分析一下:
我們首先看一下java.lang.ClassLoader抽象類中默認實現的兩個構造函數: protected ClassLoader() { SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkCreateClassLoader(); } //默認將父類加載器設置為系統類加載器,getSystemClassLoader()獲取系統類加載器 this.parent = getSystemClassLoader(); initialized = true; } protected ClassLoader(ClassLoader parent) { SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkCreateClassLoader(); } //強制設置父類加載器 this.parent = parent; initialized = true; } 我們再看一下ClassLoader抽象類中parent成員的聲明: // The parent class loader for delegation private ClassLoader parent;
聲明為私有變量的同時並沒有對外提供可供派生類訪問的public或者protected設置器接口(對應的setter方法),結合前面的測試代碼的輸出,我們可以推斷出:
(1) 系統類加載器(AppClassLoader)調用ClassLoader(ClassLoader parent)構造函數將父類加載器設置為標准擴展類加載器(ExtClassLoader)。(因為如果不強制設置,默認會通過調用getSystemClassLoader()方法獲取並設置成系統類加載器,這顯然和測試輸出結果不符。)
(2) 擴展類加載器(ExtClassLoader)調用ClassLoader(ClassLoader parent)構造函數將父類加載器設置為null。(因為如果不強制設置,默認會通過調用getSystemClassLoader()方法獲取並設置成系統類加載器,這顯然和測試輸出結果不符。)
現在我們可能會有這樣的疑問:擴展類加載器(ExtClassLoader)的父類加載器被強制設置為null了,那么擴展類加載器為什么還能將加載任務委派給啟動類加載器呢?
圖四 標准擴展類加載器和系統類加載器成員大綱視圖
圖五 擴展類加載器和系統類加載器公共父類成員大綱視圖
通過以上兩圖可以看出,標准擴展類加載器和系統類加載器及其父類(java.net.URLClassLoader和java.security.SecureClassLoader) 都沒有覆寫java.lang.ClassLoader中默認的加載委派規則---loadClass()方法。有關java.lang.ClassLoader中默認的加載委派規則前面已經分析過,如果父加載器為null,則會調用本地方法進行啟動類加載嘗試。所以,圖三中,啟動類加載器、標准擴展類加載器和系統類加載器之間的委派關系事實上是仍就成立的。
(2) 類加載雙親委派示例
以上已經簡要介紹了虛擬機默認使用的啟動類加載器、標准擴展類加載器和系統類加載器,並以三者為例結合JDK代碼對JVM默認使用的雙親委派類加載機制做了分析。下面我們就來看一個綜合的例子。首先在eclipse中建立一個簡單的java應用工程,然后寫一個簡單的JavaBean如下:
package com.latiny.bean; public class TestBean { public TestBean() {} }
在現有當前工程中另外建立一測試類(JVMTest1.java)內容如下:
package com.latiny.reflect; public class JVMTest1 { public static void main(String[] args) { try { //查看當前系統類路徑中包含的路徑條目 System.out.println(System.getProperty("java.class.path")); //調用加載當前類的類加載器(這里即為系統類加載器)加載TestBean Class typeLoaded = Class.forName("com.latiny.bean.TestBean"); //查看被加載的TestBean類型是被那個類加載器加載的s System.out.println(typeLoaded.getClassLoader()); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
輸出結果:
C:\Work\Project\Java\Eclipse\ReflectProject\bin
sun.misc.Launcher$AppClassLoader@73d16e93
測試二:
將當前工程輸出目錄下的C:\Work\Project\Java\Eclipse\ReflectProject\bin\com\latiny\bean打包進test.jar剪貼到< Java_Runtime_Home >/lib/ext目錄下(現在工程輸出目錄下和JRE擴展目錄下都有待加載類型的class文件)。再運行測試一測試代碼,結果如下:
C:\Work\Project\Java\Eclipse\ReflectProject\bin
sun.misc.Launcher$AppClassLoader@2a139a55 78
測試三和測試二輸出結果一致。那就是說,放置到< Java_Runtime_Home >/lib目錄下的TestBean對應的class字節碼並沒有被加載,這其實和前面講的雙親委派機制並不矛盾。虛擬機出於安全等因素考慮,不會加載< Java_Runtime_Home >/lib存在的陌生類,開發者通過將要加載的非JDK自身的類放置到此目錄下期待啟動類加載器加載是不可能的。做個進一步驗證,刪除< Java_Runtime_Home >/lib/ext目錄下和工程輸出目錄下的TestBean對應的class文件,然后再運行測試代碼,則將會有ClassNotFoundException異常拋出。有關這個問題,大家可以在java.lang.ClassLoader中的loadClass(String name, boolean resolve)方法中設置相應斷點運行測試三進行調試,會發現findBootstrapClass0()會拋出異常,然后在下面的findClass方法中被加載,當前運行的類加載器正是擴展類加載器(sun.misc.Launcher$ExtClassLoader),這一點可以通過JDT中變量視圖查看驗證。
6 java程序動態擴展方式
Java的連接模型允許用戶運行時擴展引用程序,既可以通過當前虛擬機中預定義的加載器加載編譯時已知的類或者接口,又允許用戶自行定義類裝載器,在運行時動態擴展用戶的程序。通過用戶自定義的類裝載器,你的程序可以裝載在編譯時並不知道或者尚未存在的類或者接口,並動態連接它們並進行有選擇的解析。
運行時動態擴展java應用程序有如下兩個途徑:
(1) 調用java.lang.Class.forName(…)
這個方法其實在前面已經討論過,在后面的問題2解答中說明了該方法調用會觸發哪個類加載器開始加載任務。這里需要說明的是多參數版本的forName(…)方法:
public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException
這里的initialize參數是很重要的,可以決定類被加載同時是否完成初始化的工作(說明:單參數版本的forName方法默認是不完成初始化的).有些場景下,需要將initialize設置為true來強制加載同時完成初始化,例如典型的就是利用DriverManager進行JDBC驅動程序類注冊的問題,因為每一個JDBC驅動程序類的靜態初始化方法都用DriverManager注冊驅動程序,這樣才能被應用程序使用,這就要求驅動程序類必須被初始化,而不單單被加載。
(2) 用戶自定義類加載器
通過前面的分析,我們可以看出,除了和本地實現密切相關的啟動類加載器之外,包括標准擴展類加載器和系統類加載器在內的所有其他類加載器我們都可以當做自定義類加載器來對待,唯一區別是是否被虛擬機默認使用。前面的內容中已經對java.lang.ClassLoader 抽象類中的幾個重要的方法做了介紹,這里就簡要敘述一下一般用戶自定義類加載器的工作流程吧(可以結合后面問題解答一起看):
① 首先檢查請求的類型是否已經被這個類裝載器裝載到命名空間中了,如果已經裝載,直接返回;否則轉入步驟2;
② 委派類加載請求給父類加載器(更准確的說應該是雙親類加載器,整個虛擬機中各種類加載器最終會呈現樹狀結構),如果父類加載器能夠完成,則返回父類加載器加載的Class實例;否則轉入步驟3;
③ 調用本類加載器的findClass() 方法,試圖獲取對應的字節碼,如果獲取的到,則調用defineClass() 導入類型到方法區;如果獲取不到對應的字節碼或者其他原因失敗,返回異常給loadClass(), loadClass() 轉拋異常,終止加載過程(注意:這里的異常種類不止一種)。
(說明:這里說的自定義類加載器是指JDK 1.2以后版本的寫法,即不覆寫改變java.lang.loadClass() 已有委派邏輯情況下)
參考:https://www.cnblogs.com/ityouknow/p/5603287.html