【深入理解Java虛擬機】類加載機制
來源:程序員人生 發布時間:2016-07-01 15:24:10 閱讀次數:2420次
本文內容來源于《深入理解Java虛擬機》1書,非常推薦大家去看1下這本書。
本系列其他文章:
【深入理解Java虛擬機】Java內存區域模型、對象創建進程、常見OOM
【深入理解Java虛擬機】垃圾回收機制
1、類加載機制概述
虛擬機把描寫類的數據從Class文件加載到內存,并對數據進行校驗、轉換解析和初始化,終究構成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。
在java中,類型的加載、連接和初始化進程都是在程序運行期間完成的,這類策略雖然會帶來1些性能開消,但是卻為java利用程序提供了高度的靈活性,java動態擴大的語言特性就是依賴運行期動態加載和動態鏈接這個特點構成的,所謂java動態擴大,比如,如果編寫了1個面向接口的利用程序,可以等到運行時再指定其實際的實現類。
2、類加載的時機
類從被加載到虛擬機內存中開始,到卸載出內存為止,全部生命周期包括:加載、驗證、準備、解析、初始化、使用、卸載,共7個階段。其中,驗證、準備、解析3個階段稱為連接(Linking),7個進程產生順序以下:
上面這7個進程,除解析這個進程外,其余進程必須循序漸進地履行,即順序是肯定的,而解析進程不1定,在某些情況下可以在初始化階段以后再履行,這是為了支持java語言的運行時綁定(也稱為動態綁定或晚期綁定)。
java虛擬機規范中,并沒有規定類加載進程中的第1個階段(即加載階段)的履行時機,但是對初始化階段,虛擬機規范中嚴格規定了“有且只有”下面5種情況下必須立即對類進行初始化(而這時候,加載、驗證、準備自然需要在此之前開始):
(1)遇到new、getstatic、putstatic、invokestatic這4條指令時,必須觸發其初始化。這4條指令最多見的場景是:使用new關鍵字實例化對象、讀取或設置1個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外,即常量除外)、調用1個類的靜態方法的時候;
(2)進行反射調用的時候;
(3)初始化1個類的時候,如果其父類還沒有初始化,則需要先觸發其父類的初始化;
(4)當虛擬機啟動時,需要先初始化那個包括main方法的要履行的主類;
(5)當使用JDK1.7的動態語言支持時,如果1個java.lang.invoke.MethodHandle實例最后的解析結果為REF_getStatic 、REF_putStatic、REF_invokeStatic的方法句柄,句柄對應的類會被初始化;
上面5種場景觸發類進行初始化的行動稱為對1個類進行“主動援用”,除此以外,所有其他援用類的方式都不會觸發初始化步驟(注意,此時已是援用了,只不過不會觸發初始化,其他階段是不是觸發要看具體虛擬機的實現),這些援用稱為“被動援用”。
被動援用的幾個例子:
(1)對靜態字段,只有直接定義這個字段的類才會被初始化,因此通過其子類來援用父類中定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。至因而否要動身子類的加載、驗證需要看具體虛擬機實現;以下:
class SuperClass{
static{
System.out.println("SuperClass init!");
}
public static int value = 123;
}
class SubClass extends SuperClass{
static{
System.out.println("SubClass init!");//子類中援用父類的靜態字段,不會致使類初始化
}
}
public class Test {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}
運行結果:
可以看到,只會打印出父類的初始化語句。
(2)通過數組定義來援用類,不會觸發此類的初始化。如 A[] ints = new A[10] , 不會觸發A 類的初始化。而是會觸發名為 LA的類初始化。它是1個由虛擬機自動生成的、直接繼承于Object 的子類,創建動作由字節碼指令 newarray 觸發。這個類代表了1個元素類型為 A 的1位數組,數組中的屬性和方法都實現在這個類中。Java 語言中數組的訪問比C/C++ 安全是由于這個類封裝了數組元素的訪問方法。以下:
public class Test {
public static void main(String[] args) {
SuperClass[] sca = new SuperClass[10];
}
}
SuperClass類為上面的那個,運行后發現并沒有打印出SuperClass init!,說明沒有觸發SuperClass類的初始化階段。
(3)常量在編譯階段會存入調用類的常量池中,本質上并沒有直接援用到定義常量的類,因此不會觸發定義常量的類的初始化,以下:
class ConstClass{
static{
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world";
}
public class Test {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);
}
}
運行結果:
只是輸出了hello world,并沒有輸出ConstClass init!,可見ConstClass類并沒有被初始化。
注意:
上面講的3個例子是被動援用的情況,很多情況下我們會通過new來初始化1個類,這個情形它屬于上面提到的5種主動援用的場景,因此會觸發這個類的初始化,如果這個類有父類的話,會先觸發父類的初始化。注意不要和上面的被動援用弄混了。
接口的初始化
上面代碼中用static語句塊進行初始化,而結構中不能使用static語句塊,但是編譯器依然回味接口生成<clinit>()類構造器來初始化接口中的成員變量(常量);接口與類初始化的區分主要是在上面5種主動援用中的第3種:當1個類在初始化時,要求其父類全部已初始化過了,但是對接口的初始化來講,并不要求其父接口全部都完成了初始化,只有在真正使用到付接口的時候(如援用接口中定義的常量)才會初始化。
3、類加載進程
3.1 加載
在加載階段,需要完成3件事情:
(1)通過1個類的全限定名來獲得其定義的2進制字節流。
(2)將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構。
(3)在內存中生成1個代表這個類的java.lang.Class對象(并沒有明確規定是在java堆中,對HotSpot虛擬機來講,Class對象比較特殊,它雖然是對象,但是寄存在方法區里面),作為對方法區中這些數據的訪問入口。
對(1),并沒有指明2進制字節流的獲得途徑,也即不1定都是從1個Class文件中獲得,還可以從以下方式獲得:
1)從緊縮包中獲得,比如 JAR包、EAR、WAR包等
2)從網絡中獲得,比如紅極1時的Applet技術
3)從運行進程中動態生成,最出名的便是動態代理技術,在java.lang.reflect.Proxy 中,就是用了 ProxyGenerator.generateProxyClass 來為特定接口生成情勢為“$Proxy”的代理類的2進制流
4)從其它文件生成,如JSP文件生成Class 類
相對類加載進程的其他階段,加載這1步驟是開發人員可控的,便可以通過自定義類加載器來控制加載進程。
對數組來講,數組類本身不通過類加載器創建,它是由Java虛擬機直接創建的,但是數組的元素類型,終究是要靠類加載器去創建。
3.2 驗證
驗證階段的目的是為了確保Class文件的字節流中包括的信息符合當前虛擬機的要求,并且不會危害虛擬機本身的安全。
Java語言本身是相對安全的,由于使用純潔的java代碼沒法做到諸如訪問數組邊界意外的數據、講1個對象轉型為它并未實現的類型、跳轉到不存在的代碼行之類的事情,如果我們這樣做了,那編譯器將謝絕編譯,也就保證了安全。但是前面說過,Class文件其實不1定要用Java源碼編譯而來,它還可以從很多途徑產生,在字節碼層面,其他方式可能能做到java代碼沒法做到的事情,因此虛擬機需要對加載盡可能的字節流進行驗證。驗證進程分為4步:
(1)文件格式驗證
這1階段是要驗證字節流是不是符合Class文件格式的規范,并且能被當前版本的虛擬機處理。包括以下這些驗證點:
- 是不是以魔數0xCAFEBABE開頭
- 主、次版本號是不是在當前虛擬機處理范圍以內
- 常量池的常量中是不是有不被支持的常量類型(檢查常量tag標志)
- 指向常量的各種索引值中是不是有指向不存在的常量或不符合類型的常量
- CONSTANT_Utf8_info 型的常量中是不是有不符合UTF8 編碼的數據
- Class 文件中各個部份和文件本身是不是有被刪除的或被附加的其它信息
...
這1階段驗證的目的是保證輸入的字節流能正確的解析并存儲到方法區中,這階段是基于2進制字節流進行的,通過驗證后,字節流才會進入到內存的方法區中進行存儲。因此,后面的3個驗證階段是基于方法區的存儲結構進行分析的,不會再直接操作字節流了。
(2)元數據驗證
對字節碼描寫的信息進行語義分析,以保證其描寫的信息符合Java語言規范的要求,主要是驗證類的繼承關系、數據類型是不是符合,驗證點包括:
- 這個類是不是有父類(除Object類外,其他所有類都應當有父類)
- 這個類的父類是不是繼承了不允許被繼承的類(final 修飾的類)
- 這個類如果不是抽象類,是不是實現了其父類或接口當中要求實現的所有方法
- 類中的字段、方法是不是和父類產生矛盾(如覆蓋了父類final 字段,出現了非法的方法重載,如方法參數1致,但返回類型卻不同)
(3)字節碼驗證
最復雜的1個階段,主要目的是通過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。在元數據驗證階段對數據類型做完校驗后,這個階段將對類的方法體進行校驗分析,以保證被校驗類的方法在運行時不會做出危害虛擬機安全的事件,有以下1些驗證點:
- 保證任什么時候候,操作數棧的數據類型與指令代碼序列都能配合工作,例如不會出現類似這樣的情況:在操作棧放入了1個int類型數據,使用時卻按 long 類型加載到本地變量表中
- 保證跳轉指令不會跳轉到方法體外的字節碼指令上
- 保證方法體中類型轉換是有效的
(4)符號援用驗證
這1階段產生在虛擬機將符號援用轉化為直接援用的時候,而這個轉化動作產生在解析階段,符號援用可以看作是對類本身之外(常量池中的各種符號援用)的信息進行匹配性校驗,驗證點以下:
- 符號援用中通過字符串描寫的全限定名是不是能找到相應的類
- 在指定類中對否存在符合方法的字段描寫符和簡單名稱所描寫的方法和字段
- 符號援用中的類、字段、方法的訪問性(private、protected、public、default)是不是可被當前類訪問
這1階段驗證的目的是確保解析動作能正常履行。
對虛擬機來講,驗證階段是1個非常重要的,但不是1定必要(由于對程序運行期沒有影響)的的階段。
3.3 準備
準備階段是正式為類變量分配內存并設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。有兩點需要注意:
(1)這階段進行內存分配的僅包括類變量(即被static修飾的變量),不包括實例變量,實例變量會在對象實例化時隨著對象1起分配在Java堆中;
(2)這里所說的初始值“通常情況”下是數據類型的零值,假定1個類變量的定義以下:
public static int value = 123;
那變量value在準備階段過后的零值為0而不是123,由于這時候候并未履行任何Java方法,把value賦值為123的動作是在初始化階段才會進行。對“非通常情況”,是指定義為常量的那些變量(即final修飾的),會在這1階段就被賦值,如:
public static final int value = 123;
此時在準備階段過后,value的值將會被賦值為123。
3.4 解析
解析階段是虛擬機將常量池中的符號援用轉化為直接援用的進程。
- 符號援用(Symbolic References):即用1組符號來描寫所援用的目標。它與虛擬機的內存布局無關,援用的目標不1定已加載到內存中。
- 直接援用(Direct References):直接援用可以是指向目標的指針、相對偏移量或是1個能間接定位到目標的句柄。它是和虛擬機內存布局相干的,如果有了直接援用,那援用的目標一定已在內存中存在了。
解析動作主要針對 類或接口、字段、類方法、接口方法、方法類型、方法句柄 和 調用限定符 7類符號援用進行。
(1)類或接口的解析
判斷所要轉化成的直接援用是對數組類型,還是普通的對象類型的援用,從而進行不同的解析。
(2)字段解析
在對字段進行解析前,會先查看該字段所屬的類或接口的符號援用是不是已解析過,沒有就先對字段所屬的接口或類進行解析。在對字段進行解析的時候,先查找本類或接口中是不是有該字段,有就直接返回;否則,再對實現的接口進行遍歷,會依照繼承關系從下往上遞歸(也就是說,每一個父接口都會走1遍)搜索各個接口和它的父接口,返回最近1個接口的直接援用;再對繼承的父類進行遍歷,會依照繼承關系從下往上遞歸(也就是說,每一個父類都會走1遍)搜索各個父類,返回最近1個父類的直接援用。
(3)類方法解析
和字段解析搜索步驟差不多,只不過是先搜索父類,再搜索接口。
(4)接口方法解析
和類方法解析差不多,只不過接口中不會有父類,因此只需要對父接口進行搜索便可。
3.5 初始化
初始化是類加載進程的最后1步,此階段才開始真正履行類中定義的Java程序代碼(或說字節碼,也僅限與履行<clinit>()方法)。在準備階段,我們已給變量付過1次系統要求的初始值(零值),而在初始化階段,則會根據程序員的意愿給類變量和其他資源賦值。主要是通過<clinit>()方法來履行的:
(1)<clinit>()方法是由編譯器自動搜集類中的所有類變量的賦值動作和靜態語句塊中的語句合并產生的,編譯器搜集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變量,定義在它以后的變量,在前面的靜態語句中可以賦值,但是不能訪問。以下:
public class Test {
static{
i = 0;//可以給變量賦值,編譯通過
System.out.println(i);//編譯不通過!!不能進行訪問后面的靜態變量
}
static int i =1;
}
有點與我們平常的認知相反,這里是可以下賦值,卻不能訪問...
(2)<clinit>()方法與實例構造器<init>()方法(類的構造函數)不同,它不需要顯式地調用父類構造器,虛擬機會保證在子類的<clinit>()方法履行之前,父類的<clinit>()方法已履行終了。因此,在虛擬機中第1個被履行的<clinit>()方法的類肯定是java.lang.Object。
(3)<clinit>()方法對類或接口來講其實不是必須的,如果1個類中沒有靜態語句塊,也沒有對類變量的賦值操作,那末編譯器可以不為這個類生成<clinit>()方法。
(4)接口中不能使用靜態語句塊,但依然有類變量(final static)初始化的賦值操作,因此接口與類1樣會生成<clinit>()方法。但是接口與類不同的是:履行接口的<clinit>()方法不需要先履行父接口的<clinit>()方法,只有當父接口中定義的變量被使用時,父接口才會被初始化。另外,接口的實現類在初始化時也1樣不會履行接口的<clinit>()方法。
(5)虛擬機會保證1個類的<clinit>()方法在多線程環境中被正確地加鎖和同步,如果多個線程同時去初始化1個類,那末只會有1個線程去履行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程履行<clinit>()方法終了。如果在1個類的<clinit>()方法中有耗時很長的操作,那便可能造成多個線程阻塞,在實際利用中這類阻塞常常是很隱蔽的。
4、類加載器
前面說過,在類加載進程的第1個階段:加載階段,除可使用系統提供的引導類加載器外,還可使用用戶自定義的類加載器,以便讓用戶決定如何去獲得所需要的類(是從Class文件中?還是從jar、或其他方式...可以自由決定)。
4.1 類和類加載器
任意1個類,都需要由加載它的類加載器和這個類本身共同肯定其在Java 虛擬機中的唯1性,每個類加載器,都具有1個獨立的類名稱空間。這句話可以表達的更通俗1些:比較兩個類是不是相等,只有在這兩個類是同1個類加載器加載的條件下才意義。否則,即便這兩個類來自同1個Class文件,被同1個虛擬機加載,但只要加載他們的類加載器不同,那這兩個類就一定不相等。
這里的“相等”,包括代表類的 Class 對象的equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回結果,也包括 instanceof 關鍵字對對象所屬關系判定等情況。下面代碼演示了不同類加載器對 instanceof 關鍵字運算的結果的影響。
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name)
throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1)
+ ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
Class c = myLoader.loadClass("org.bupt.xiaoye.blog.ClassLoaderTest");
Object obj = c.newInstance();
System.out.println(obj.getClass());
System.out.println(ClassLoaderTest.class);
System.out.println(obj instanceof ClassLoaderTest);
}
}
運行結果以下:
class org.bupt.xiaoye.blog.ClassLoaderTest
class org.bupt.xiaoye.blog.ClassLoaderTest
false
我們使用了1個自定義的類加載器去加載ClassLoaderTest,由第1句也能夠看出這個對象也的確是ClassLoaderTest實例化出來的對象,但是這個對象在與類class org.bupt.xiaoye.blog.ClassLoaderTest 做屬性檢查的時候卻反悔了false,這就是由于虛擬機中存在了兩個ClassLoaderTest類,1個由系統利用程序類加載器加載,1個由我們自定義的類加載器加載,雖然是 來自同1個Class文件,但仍然是兩個獨立的類。
因此,類是不是相等,取決于類本身和加載該類的類加載器是不是是同1個類加載器。
4.2 雙親委派模型
從虛擬機的角度來說,只存在兩種不同的類加載器:
1種是啟動類加載器(Bootstrap ClassLoader),這個類加載器用 C++ 語言實現, 是虛擬機本身的1部份:
另外一種就是所有其它的類加載器, 這些類加載器用Java 語言實現,獨立于虛擬機外部,并且全都繼承與抽象類 java.lang.ClassLoader。
從Java 開發人員的角度來看,類加載器還可以劃分的更細致1些,絕大多數Java 程序都會用到以下3種系統提供的類加載器:
(1)啟動類加載器(Bootstrap ClassLoader) : 這個類加載器負責將寄存在 <JAVA_HOME>\lib 目錄中的,或被 -Xbootclasspath 參數指定的路徑中的,并且是虛擬機辨認的(僅依照文件名辨認,如rt.jar ,名字不符合類庫不會加載) 類庫加載到虛擬機內存中。啟動類加載器沒法被 java 程序直接援用,如需要,直接使用 null 代替便可。
(2)擴大類加載器(Extension ClassLoader):這個加載器由sun.misc.Launcher$ExtClassLoader 實現,它負責加載<JAVA_HOME>\lib\ext 目錄中的,或被 java.ext.dirs 系統變量所指定的路徑中的所有類庫,開發者可以直接使用擴大類加載器。
(3)利用程序類加載器(Application ClassLoader):這個類加載器由 sun.misc.Launcher$AppClassLoader 實現。這個這個類加載器是 ClassLoader 中的getSystemClassLoader() 方法的返回值,所以1般稱它為系統類加載器。它負責加載用戶路徑(ClassPath)上所指定的類庫,開發者可使用這個類加載器,如果利用程序沒有自定義過自己的類加載器,1般情況下這個就是程序中默許的類加載器。
我們的利用程序都是由這3中類加載器相互配合進行加載的,如果有必要,還可以加入自己定義的類加載器。這些類加載器之間的關系1般以下圖所示:
圖中的類加載器之間的這類層次關系,稱為類加載器的雙親委派模型。雙親委派模型要求除頂層的啟動類加載器,其余的類加載器都應當有自己的父類加載器。這里類加載器之間的父子關系1般不會以繼承關系來實現,而是使用組合關系來復用父加載器的代碼。
雙親委派模型的工作進程是:如果1個類加載器收到了類加載器的要求,它首先不會自己嘗試加載這個類,而是把這個要求委派給父類加載器去完成,每個層次的類加載器都是如此,因此所有的加載要求終究都應當傳送到頂層的啟動類加載器中,只有當父類加載器反饋自己沒法完成這個加載要求(它的搜索范圍中沒有找到所需的類時),子加載類才會嘗試自己去加載。
使用雙親委派模型的好處:就是Java類隨著它的類加載器1起具有了1種帶有優先級的層次關系。比如對類Object來講,它寄存在rt.jar中,不管哪個類加載器要加載這個類,終究都是委派給處于模型最頂真個啟動類加載器去加載,因此Object類在程序中的各種類加載器環境中都是同1個類。相反,如果沒有使用雙親委派模型,由各個類自己去加載的話,依照我們前面說的,如果用戶自己編寫了1個Object類,并放在程序的ClassPath中,那系統中將會出現多個不同的Object類,此時Java類型提示中最基礎的行動也就沒法保證了,利用程序也將變得混亂。
因此,雙親委派模型對保證Java程序的穩定運作很重要,但是他的實現其實很簡單,實現雙親委派模型的代碼幾種在java.lang.ClassLoader的loadClass()方法當中,邏輯清晰易懂:先檢查類是不是被加載過,若沒有則調用父加載器的loadClass() 方法,若父加載器為空則默許使用啟動類加載器作為父加載器。如果父加載器失敗,拋出 ClassNotFoundException 異常后,再調用自己的 finClass() 方法進行加載,以下:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// 首先檢查類是不是已被加載過
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 調用父類加載器加載
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
//父類加載器沒法完成加載,調用本身的加載器加載
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(
t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(
t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
(注:文中圖片來源于:
http://blog.csdn.net/zq602316498/article/details/38871785
http://blog.csdn.net/zq602316498/article/details/38902355
)
生活不易,碼農辛苦
如果您覺得本網站對您的學習有所幫助,可以手機掃描二維碼進行捐贈