目錄
1,jvm記憶體結構的布局
2,類加載子系統的作用
3,類加載器(class loader)
4,類的加載過程
4.1,類的加載階段(狹義上的加載)
4.2,類的連結
4.2.1,驗證階段(Verify)
4.2.2,準備階段(prepare)
4.2.3,解析階段(Resolve)
4.3,初始化階段
5,類的加載器
5.1,加載器的分類
5.2,加載器的介紹
5.3,擷取ClassLoader的途徑
6,雙親委派機制
6.1,jdbc加載舉例
6.2,雙親委派機制的優勢及作用
6.3,修改核心類例子
7,沙箱安全機制
8,其他概念
1,jvm記憶體結構的布局

看看jvm的記憶體布局,我們的java程式經過編譯器編譯為位元組碼檔案之後,這些位元組碼檔案描述的資料資訊是需要被加載到虛拟機中才可以被運作使用,而上面圖上的類加載子系統就是負責把我們的位元組碼檔案加載到虛拟機的固定區域。在java語言中類的加載,連接配接和初始化都是在程式的運作期間完成的,java可以動态擴充的語言 的特性就是依賴運作時期動态加載和動态連結這個特點實作的。
- 在看來加載子系統之前,我們先來看看jvm的全貌,這有利于加深我們對jvm的了解。
- 下面這張全局圖對上面進行翻譯:
- 加載位元組碼檔案經過三個步驟:加載--->連結(驗證---準備---解析)--->初始化,嚴格來說,一個位元組碼檔案從被加載到虛拟機中開始直到被解除安裝出記憶體為止,完整的聲明周期要經過:加載---連結(驗證---準備---解析)---初始化---使用---解除安裝等過程。
- 方法區隻有hotspot虛拟機有,另外兩大商業虛拟機沒有。
2,類加載子系統的作用
作用:
- 類加載子系統負責從本地檔案或者網絡檔案中加載class檔案,class檔案開頭有特定的辨別符。
- classloader負責class檔案的加載,至于他是否可以運作,由執行引擎決定execution engine。
- 加載的類資訊存放在一塊稱為方法區的記憶體空間(也可以說是堆記憶體空間),除了類的資訊外,方法區還會存放運作時常量池資訊,可能還包括字元串常量和數字常量(這一部分常量資訊是class檔案中常量池部分的記憶體映射)。
- 加載完成後,java虛拟機外部的二進制位元組流就會按照虛拟機所設定的格式存儲在方法區之中,方法區的資料格式存儲完全是由具體的虛拟機實作而确定的。然後會在java的堆記憶體中生成一個Class對象,這個對下行作為程式通路方法區的類型資料的外部入口。
類加載的過程:
3,類加載器(class loader)
- class file存儲于本地磁盤上面,可以了解為設計師畫在紙上的模闆,而最終這個模闆在執行的時候要加載到jvm當中,根據這個模闆執行個體化n多個一模一樣的執行個體。
- class file加載到jvm當中,b被稱為DNA元素的模闆,存儲在方法區。
- 在.class--->jvm---->最終成為中繼資料模闆,此過程隻要一個運輸工具,類裝載器(class loader),扮演者一個快遞員的角色。
- 其中class檔案加載到記憶體中是以二進制流的方式進行加載,一個對象通過getclass()方法還可以擷取是哪一個類的對象。
4,類的加載過程
4.1,類的加載階段(狹義上的加載)
加載階段(Loading)
- 通過一個類的權限定名,擷取定義此類的二進制位元組流。
- 将這個位元組流所代表的靜态存儲結構轉化為方法區(jdk7前叫做永久代,之後叫做中繼資料空間,都是方法區的落地實作)的運作時資料結構。
- 在記憶體中生成一個代表這個類的java.lang.class對象,作為方法區這個類的各種資料結構的通路入口。
4.2,類的連結
4.2.1,驗證階段(Verify)
- 為什麼需要驗證階段?
為什麼要進行驗證,因為從java語言的角度來看,寫好的程式經過編譯之後的位元組碼檔案應該是沒有什麼問題的,但是從jvm角度來看,讀取到的class檔案可以從任意地方,這就導緻可能讀入危害jvm的位元組碼檔案,是以需要進行驗證階段,檢查位元組碼檔案是否是安全的。
目的在于確定.class檔案的位元組流中包含的資訊符合目前的虛拟機的要求,保證被加載的正确性,不會危害虛拟機自身的安全。
- 主要包括四種驗證方式
- 檔案格式驗證,不同檔案檔案頭不一樣。(主要驗證位元組流是否符合class檔案格式的規範,也就是保證資料可以被存儲到資料區裡面,後面三部分驗證都是基于這個驗證之上的)。
- 中繼資料驗證。(也就是語義的檢驗,要求語義符合java語言的規範)
- 位元組碼驗證。(通過資料流分析和控制流分析,确定語義是合法的,符合邏輯)
- 符号引用驗證(也就是将符号引用轉換為直接引用)
4.2.2,準備階段(prepare)
- 為類變量(static修飾的變量)設定記憶體和并且設定該類變量的初始值,即0。注意:這裡的初始值是8中基本資料類型和一種引用類型的基本初始值,此時還是在jvm層面的初始化,還沒有執行java代碼的任何構造函數,是以是從虛拟機角度對變量進行初始化。
- 這裡不包含用final修飾的static(也就是常量),因為final修飾的變量在編譯階段就已經配置設定空間了,也就是已經放入方法區的常量池之中,準備階段會顯示初始化。
- 這裡不會為執行個體變量配置設定初始化(此時還沒有建立對象),類變量會配置設定在方法區中,而執行個體變量會随着對象一起被配置設定到java的堆中。
4.2.3,解析階段(Resolve)
- 将常量池中的符号引用轉換為直接引用的過程。
- 解析操作往往會伴随着jvm在執行完初始化之後再執行。
- 符号引用就是一組符号來描述所引用的目标,符号引用的字面量形式明确定義java的class檔案中,直接引用就是直接指向目标的指針,相對偏移量或者一個間接定位到目标的句柄。
- 解析動作主要針對類或者接口,字段,類方法,接口方法,方法類型等,對應常量池中的constant_class_info,constant_fieldref_info,constant_Methodref_info等。
4.3,初始化階段
- 初始化方法就是執行類構造器方法clinit()過程。(注意這裡是執行類構造器clinit()的過程)
- 此方法不需要自己定義,是javac編譯器中自動收集類中所有的類變量的指派動作和靜态代碼塊中的語句合并而來的。如果沒有這種操作,也就沒有clinit()方法,
。我們注意到如果沒有靜态變量c,那麼位元組碼檔案中就不會有clinit方法
- 構造方法中的指令按照源檔案中出現的順序執行。(也就是所有變量的指派操作會按照檔案中指派順序依次指派,後面的指派會覆寫前面的指派)。
- clinit()方法不同于類的構造器,構造器是虛拟機視角下的init()方法。
- 如果該類具有父類,jvm會保證子類的clinit()方法執行前,父類的clinit()方法已經執行完畢,是以父類中定義的靜态代碼塊一定要早于子類變量的指派操作,這點在寫程式時需要小心。
- 虛拟機必須保證一個類的clinit()方法在多線程下被同步加鎖。
- 如果類或者方法中沒有給變量指派或者靜态代碼塊,那麼就沒有調用此方法,對每一個類進行反編譯都會産生一個init()方法,init()方法對應的就是類的構造器的方法。
- 可以從另一個角度去了解初始化階段:初始化過程就是執行類構造器clinit()方法的過程,此方法并不是程式員寫的,而是編譯器自動生成的,此方法與類的構造函數不同,也就是init()方法,clinit()方法不需要顯示的斯奧用父類的構造器,但是構造方法必須調用父類的構造器。
- clinit()方法對于一個類來說并不是必須的,如果一個類中沒有靜态代碼塊或者類變量,也就沒有此方法,接口中不能有靜态代碼塊,但是仍然有變量初始化的指派操作,是以接口和類一樣會生成clinit()方法,但是和類不同的是,執行接口的clinit()方法前不需要先執行其父類的clinit()方法,因為隻有父類接口中的變量被使用的時候,才會被初始化,另外,接口的實作類在初始化的時候也不一定要執行接口的clinit()方法。
5,類的加載器
5.1,加載器的分類
java支援兩種類型的類加載器:
- 引導型類加載器(bootstrap classloader)
- 自定義類加載器(user-define-classloader)
- 自定義類加載器是所有派生于抽象類classloader(也就是應用類型加載器)的類加載器。
- 系統中類加載器的組織架構:
Jvm系列-類加載子系統(二)1,jvm記憶體結構的布局2,類加載子系統的作用3,類加載器(class loader)4,類的加載過程5,類的加載器6,雙親委派機制7,沙箱安全機制8,其他概念
extension class loader和system class loader都屬于使用者自定義加載器,應為他們都是繼承自class loader,也就是隻要是繼承class loader的加載器,都是使用者自定義加載器。
對于使用者自定義類來說:使用系統類加載器AppClassLoader進行加載
java核心類庫都是使用引導類加載器BootStrapClassLoader加載的
- 系統類加載器的示範
public class ClassLoaderTest {
public static void main(String[] args) {
//擷取系統類加載器
ClassLoader classLoader=ClassLoader.getSystemClassLoader();
//列印系統類加載器對象的引用位址:[email protected]
System.out.println(classLoader);
//擷取其父類加載器,即擴充類加載器
ClassLoader parentClassLoader=classLoader.getParent();
System.out.println(parentClassLoader);
//擷取bootstraploader加載器
ClassLoader parent = parentClassLoader.getParent();
System.out.println(parent);
//對用于自定義類來說,是有哪一個加載器加載的呢?
ClassLoader classLoader1 = ClassLoaderTest.class.getClassLoader();
System.out.println(classLoader1);
//檢視String有那個加載器加載
ClassLoader classLoader2 = String.class.getClassLoader();
System.out.println(classLoader2);
}
}
[email protected]//系統類加載器,也叫作應用類加載器
[email protected]//擴充類加載器
null
[email protected]//使用者自定義類有系統加載器加載
null//String類目前的加載器為null,是以string也是有擷取bootstraploader加載器類加載器進行加載,系統核心的類庫全部是由擷取bootstraploader核心加載器進行加載
//擷取加載器是null的話,都是由引導類加載器加載的
5.2,加載器的介紹
sun.misc.Launcher是java虛拟機的一個入口
- 啟動類加載器(引導類加載器 Bootstrap ClassLoader)
- 這個類加載器是由c&c++語言實作的,嵌套在java虛拟機内部。
- 此加載器用來加載java的核心庫(JAVA_HOME/jre/lib/rt.jar)也即是負責加載lib檔案夾下面的内容,用于提供jvm自身需要的類,
- 此加載器并不繼承自java.lang.classloader,沒有父類加載器。
- 加載擴充類和應用程式類加載器,并指定為他們的父類加載器。
- 處于安全考慮,bootstrap加載器僅僅加載包名為java,javax,sun等開頭的類。
- 擴充類加載器(extention classloader)
- java語言編寫的加載器,由sun.misc.launcher$EXTclassloader實作。
- 派生于classloader抽象類。
- 父類加載器是啟動類加載器。
- 從java.ext.dirs系統目錄下加載類庫,或者從jdk的安裝目錄jre/lib/ext/子目錄下加載類庫,如果使用者建立的jar包放在此目錄下面,也會自動有擴充類加載器進行加載。
- 應用程式類加載器(系統類加載器appclassloader)
- java語言編寫的加載器,由sun.misc.launcher$Appclassloader實作。
- 派生于classloader抽象類。
- 父類加載器為擴充類加載器。
- 他負責加載環境變量為classpath或者系統屬性java.class.path指定路徑下的類庫。
- 該類加載器是系統中預設的加載器,一般來說java應用程式的類都是由系統類加載器完成加載的。
- 通過classloader#getsystemclassloader方法可以擷取到該類加載器。
- 使用者自定義類加載器
- 為什麼要自定義類加載器?
- 隔離加載類
- 修改類加載方式。
- 擴充加載源。
- 防止源碼洩露。
- 自定義類加載器的步驟:
- 開發人員可以通過繼承java.lang.classloader的方式,實作自己的類加載器。
- 在jdk1.2之前,在自定義類加載器時候,總會去繼承classloader類并且重寫loaderClass()方法,進而實作自定義類加載器,在jdk1.2之後,已不再建議使用者去覆寫loaderClass()方法,而是把自定義類的加載邏輯寫在findclass()方法中。
- 在編寫自定義類加載器時候,如果沒有太過複雜的要求,可以直接繼承URLclassloader類,這樣可以避免自己去寫findclass()方法,及其擷取位元組碼流的方式,使自定義類加載器更加友善。
- 為什麼要自定義類加載器?
- 代碼示範
/**
* 虛拟機自帶加載器
*/
public class ClassLoaderTest1 {
public static void main(String[] args) {
System.out.println("********啟動類加載器*********");
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
//擷取BootStrapClassLoader能夠加載的api路徑
for (URL e:urls){
System.out.println(e.toExternalForm());
}
//從上面的路徑中随意選擇一個類 看看他的類加載器是什麼
//Provider位于 /jdk1.8.0_171.jdk/Contents/Home/jre/lib/jsse.jar 下,引導類加載器加載它
ClassLoader classLoader = Provider.class.getClassLoader();
System.out.println(classLoader);//null
System.out.println("********拓展類加載器********");
String extDirs = System.getProperty("java.ext.dirs");
for (String path : extDirs.split(";")){
System.out.println(path);
}
//從上面的路徑中随意選擇一個類 看看他的類加載器是什麼:拓展類加載器
ClassLoader classLoader1 = CurveDB.class.getClassLoader();
System.out.println(classLoader1);//[email protected]
}
}
5.3,擷取ClassLoader的途徑
- 代碼示範
public class TestClassLoader {
public static void main(String[] args) throws ClassNotFoundException {
// 擷取類加載器的第一種方式
// 加載string到記憶體隻是擷取記憶體中一個大的Class對象,也就是String類結構資訊
// 系統類使用啟動類加載器,是以傳回結果是null
ClassLoader classLoader=Class.forName("java.lang.String").getClassLoader();
System.out.println(classLoader);
// 第二種方式,擷取目前線程上下文的加載器
ClassLoader classLoader1=Thread.currentThread().getContextClassLoader();
System.out.println(classLoader1);
// 第三種方式
ClassLoader classLoader2=ClassLoader.getSystemClassLoader().getParent();
System.out.println(classLoader2);
}
}
6,雙親委派機制
- 雙親委派機制原理圖
- java虛拟機對class位元組碼檔案采用的是按需加載的方式,也就是說當需要該類的時候才會把該類的位元組碼檔案加載到記憶體生成class對象,而且加載某一個類的class對象的時候,java虛拟機采用的是雙親委派機制,即把請求交給父類處理,它是一種任務委派模式。(如果父類處理不了請求,那麼就在逐層向下,直到類加載為止)。
- 什麼是雙親委派模型?
- 如果一個類加載器收到類的加載請求,他并不會自己先去加載,而是把這個請求先委托給自己的父類去執行。
- 如果父類加載器還存在其父類加載器,則進一步向上委托,依次遞歸,請求最終會到達最頂層的啟動類加載器。
- 如果父類加載器可以完成加載任務,就成功傳回,倘若父類加載器無法完成此類的加載任務,子加載器才會嘗試自己去加載,這就是雙親委派模型。
- 比如說自己定義的類,本身由系統類加載器(app類加載器)進行加載,但是系統類加載器不會直接加載,先向上傳遞給擴充類加載器,擴充類加載器在向上傳遞給啟動類加載器,因為啟動類加載器沒有父類加載器,是以他就嘗試自己加載,但是發現自己不能加載,然後他就向下傳遞給擴充類加載器,但是擴充類加載器發現自己也不能加載,就在向下傳遞,最終由系統類加載器進行加載。(擴充類加載器和啟動類加載器有自己的類的加載目錄,不在此目錄中,這兩個加載器就不會加載)。
- 類加載器這種上下層關系不是繼承的一種關系,而是通過一種組合的關系複用父加載器的方式。
- 為什麼要使用雙親委派機制進行類的加載?
- 使用這種方式使得java中的類随着他的類加載器一起具備了一種帶有優先級的層次關系,會保證系統的類不會受到惡意的攻擊。
- 雙親委派機制的優勢
- 避免類的重複加載
- 保護程式安全,防止核心API被随意篡改(就像上面修改string類一樣,不被允許)
- 自定義類:java.lang.String
- 自定義類:java.lang.MeDsh(java.lang包需要通路權限,阻止我們用包名自定義類)
6.1,jdbc加載舉例
某一個程式要用到spi接口,那麼spi中一些核心的jar包由引導類加載器進行加載,而核心的jar包是一些接口,要具體加載一些實作類,并且是第三方的實作類,不屬于核心的api可以利用反向委派機制,由系統類加載器進行加載第三方的一些類,這裡的系統加載器實際上是線程的上下文類加載器。線程上下文加載器實際上是一些系統加載器。
6.2,雙親委派機制的優勢及作用
- 避免類的重複加載。(保證每一個類隻有一個類加載器進行加載)
- 保護程式安全,防止核心api被篡改。(也就是說使用和系統一樣的包名的話,如果在這個包下定義一個類,會報錯,系統包名下的類是啟動類加載器加載,但是系統類加載器在加載時會去找系統包名下的這個類,發現沒有就會報錯,也就是防止核心api被任意修改)
6.3,修改核心類例子
如圖,雖然我們自定義了一個java.lang包下的String嘗試覆寫核心類庫中的String,但是由于雙親委派機制,啟動加載器會加載java核心類庫的String類(BootStrap啟動類加載器隻加載包名為java、javax、sun等開頭的類),而核心類庫中的String并沒有main方法
7,沙箱安全機制
自定義string類,但是在加載自定義string類的時候會率先使用引導類型加載器進行加載,而引導類型加載器在加載的過程中會率先加載jdk自帶的檔案,(rt.jar包中的java/lang/String.jar),報錯提示沒有main方法,這就是應為加載的是(rt.jar包中的java/lang/String.jar)下的string類,這樣可以保證對java核心源代碼的保護,這就是沙箱安全機制。
8,其他概念
- 在jvm中标示兩個class對象(說的是記憶體中大的class對象,也就是類結構)是否為同一個類存在兩個必要的條件:
- 類的完整類名(也就是權限定名稱)必須完全一緻,包括包名。
- 加載這個類的classloader也必須相同。(指的是classloader的執行個體對象)。
- 換句話說,在jvm中,即使兩個類對象(class對象),來源于同一個class檔案,被同一個虛拟機加載,但是隻要加載他們的classloader執行個體對象不同,那麼這兩個對象也不相等。
- jvm必須知道一個類型是由啟動加載器加載的還是由使用者類加載器加載的,如果一個類型是由使用者類加載器進行加載,那麼jvm會将這個類的加載器的一個引用作為類型資訊的一部分儲存在方法區中,當解析一個類型到另一個類型的引用的時候,jvm需要保證這兩個類型的類加載器是相同的。
- java程式對類的使用方式分為:主動使用和被動使用
- 主動使用:七中情況,會導緻類的初始化操作。
- 建立類的執行個體。
- 通路某一個類或接口的靜态變量,或者對該靜态變量指派。
- 調用類的靜态方法。
- 反射機制(Class.forname(com.rzf.Test))
- 初始化一個類的子類。
- java虛拟機啟動時被标明為啟動類的類。
- jdk7開始提供的動态語言支援:
- java.lang.invoke.MethodHandle執行個體的解析結果。
- 除了以上七中情況,其他使用java類的方式都被看作是類的被動使用,都不會導緻類的初始化。
- 主動使用:七中情況,會導緻類的初始化操作。