JVM和ClassLoader

JVM和ClassLoader

2019-11-08java

目錄算法

1 JVM架構總體架構
  1.1 類加載器子系統
    1.1.1 加載
    1.1.2 連接
    1.1.3 初始化
  1.2 運行時數據區(Runtime Data Area)
  1.3 執行引擎
  1.4 示例
2 classloader加載class文件的原理和機制
  2.1 Classloader 類結構分析
  2.2 實現類的熱部署
  2.3 類加載器的雙親委派模型
  2.4 類加載的三種方式
  2.5 自定義類加載器的兩種方式
參考數組

 

 

1 JVM架構總體架構


 

  返回安全

圖1 JVM總體架構圖網絡

JVM被分爲三個主要的子系統:數據結構

  • 類加載器子系統
  • 運行時數據區
  • 執行引擎

1.1 類加載器子系統 


  返回架構

圖2 類加載器jvm

Java的動態類加載功能是由類加載器子系統處理。當它在運行時(不是編譯時)首次引用一個類時,它加載、連接並初始化該類文件。ide

1.1.1 加載

加載器:類由此組件加載。啓動類加載器 (BootStrap class Loader)、擴展類加載器(Extension class Loader)和應用程序類加載器(Application class Loader) 這三種類加載器幫助完成類的加載。函數

  • 啓動類加載器 – 負責從啓動類路徑中加載類,無非就是rt.jar。這個加載器會被賦予最高優先級。
  • 擴展類加載器 – 負責加載ext 目錄(jrelib)內的類.
  • 應用程序類加載器 – 負責加載應用程序級別類路徑,涉及到路徑的環境變量等etc.上述的類加載器會遵循委託層次算法(Delegation Hierarchy Algorithm)加載類文件,這個在後面進行講解。 

加載過程:

  1. 經過類的全限定名來獲取定義此類的二進制字節流
  2. 將這個類字節流表明的靜態存儲結構轉爲方法區的運行時數據結構
  3. 在堆中生成一個表明此類的java.lang.Class對象,做爲訪問方法區這些數據結構的入口。

1.1.2 連接 

校驗: 字節碼校驗器會校驗生成的字節碼是否正確,若是校驗失敗,咱們會獲得校驗錯誤。

  • 文件格式驗證:基於字節流驗證,驗證字節流符合當前的Class文件格式的規範,能被當前虛擬機處理。驗證經過後,字節流纔會進入內存的方法區進行存儲。
  • 元數據驗證:基於方法區的存儲結構驗證,對字節碼進行語義驗證,確保不存在不符合java語言規範的元數據信息。
  • 字節碼驗證:基於方法區的存儲結構驗證,經過對數據流和控制流的分析,保證被檢驗類的方法在運行時不會作出危害虛擬機的動做。
  • 符號引用驗證:基於方法區的存儲結構驗證,發生在解析階段,確保可以將符號引用成功的解析爲直接引用,其目的是確保解析動做正常執行。換句話說就是對類自身之外的信息進行匹配性校驗。

準備:分配內存並初始化默認值給全部的靜態變量。
public static int value=33;

這據代碼的賦值過程分兩次,一是上面咱們提到的階段,此時的value將會被賦值爲0;而value=33這個過程發生在類構造器的<clinit>()方法中。

解析:全部符號引用被方法區(Method Area)的直接引用所替代。

舉個例子來講明,在com.sbbic.Person類中引用了com.sbbic.Animal類,在編譯階段,Person類並不知道Animal的實際內存地址,所以只能用com.sbbic.Animal來表明Animal真實的內存地址。在解析階段,JVM能夠經過解析該符號引用,來肯定com.sbbic.Animal類的真實內存地址(若是該類未被加載過,則先加載)。

主要有如下四種:類或接口的解析,字段解析,類方法解析,接口方法解析

解析理解:

常量池中主要存放兩大類常量:字面量和符號引用。字面量比較接近於Java層面的常量概念,如文本字符串、被聲明爲final的常量值等。而符號引用總結起來則包括了下面三類常量:

  • 類和接口的全限定名
  • 字段的名稱和描述符
  • 方法的名稱和描述符

其中:

  • 全限定名:就是完整類名把.改成/。
  • 描述符:字段的類型,方法的返回類型和參數列表(參數列表又包含每一個參數的類型)。

符號引用和直接引用的區別與關聯:

  • 符號引用:符號引用以一組符號來描述所引用的目標,符號能夠是任何形式的字面量,只要使用時能無歧義地定位到目標便可。符號引用與虛擬機實現的內存佈局無關,引用的目標並不必定已經加載到了內存中。
  • 直接引用:直接引用能夠是直接指向目標的指針、相對偏移量(看下圖6)或是一個能間接定位到目標的句柄(看下圖5)。直接引用是與虛擬機實現的內存佈局相關的,同一個符號引用在不一樣虛擬機實例上翻譯出來的直接引用通常不會相同。若是有了直接引用,那說明引用的目標一定已經存在於內存之中了。

1.1.3 初始化

這是類加載的最後階段,這裏全部的靜態變量會被賦初始值, 而且靜態塊將被執行。

java中,對於初始化階段,有且只有如下五種狀況纔會對要求類馬上初始化:

  • 使用new關鍵字實例化對象、訪問或者設置一個類的靜態字段(被final修飾、編譯器優化時已經放入常量池的例外)、調用類方法,都會初始化該靜態字段或者靜態方法所在的類;
  • 初始化類的時候,若是其父類沒有被初始化過,則要先觸發其父類初始化;
  • 使用java.lang.reflect包的方法進行反射調用的時候,若是類沒有被初始化,則要先初始化;
  • 虛擬機啓動時,用戶會先初始化要執行的主類(含有main);
  • jdk 1.7後,若是java.lang.invoke.MethodHandle的實例最後對應的解析結果是 REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄,而且這個方法所在類沒有初始化,則先初始化;

1.2 運行時數據區(Runtime Data Area)


  返回

  • The 運行時數據區域被劃分爲5個主要組件:
  • 方法區 (線程共享) 常量 靜態變量 JIT(即時編譯器)編譯後代碼也在方法區存放
  • 堆內存(線程共享) 垃圾回收的主要場地
  • 程序計數器 當前線程執行的字節碼的位置指示器
  • Java虛擬機棧(棧內存) :保存局部變量,基本數據類型以及堆內存中對象的引用變量
  • 本地方法棧 (C棧):爲JVM提供使用native方法的服務

圖4 運行時數據區

 

1.3 執行引擎


  返回

分配給運行時數據區的字節碼將由執行引擎執行。執行引擎讀取字節碼並逐段執行。

解釋器:解釋器能快速的解釋字節碼,但執行卻很慢。 解釋器的缺點就是,當一個方法被調用屢次,每次都須要從新解釋。

編譯器:JIT編譯器消除了解釋器的缺點。執行引擎利用解釋器轉換字節碼,但若是是重複的代碼則使用JIT編譯器將所有字節碼編譯成本機代碼。本機代碼將直接用於重複的方法調用,這提升了系統的性能。

  • 中間代碼生成器– 生成中間代碼
  • 代碼優化器– 負責優化上面生成的中間代碼
  • 目標代碼生成器– 負責生成機器代碼或本機代碼d. 探測器(Profiler) – 一個特殊的組件,負責尋找被屢次調用的方法。

垃圾回收器: 收集並刪除未引用的對象。能夠經過調用"System.gc()"來觸發垃圾回收,但並不保證會確實進行垃圾回收。JVM的垃圾回收只收集哪些由new關鍵字建立的對象。因此,若是不是用new建立的對象,你可使用finalize函數來執行清理。Java本地接口 (JNI): JNI會與本地方法庫進行交互並提供執行引擎所需的本地庫。本地方法庫:它是一個執行引擎所需的本地庫的集合。

1.4 示例


  返回

經過如下代碼看JVM類加載執行過程

 1 package com.example.demo.classloader;
 2 /**
 3  * 從JVM調用的角度分析java程序堆內存空間的使用:
 4  * 當JVM進程啓動的時候,會從類加載路徑中找到包含main方法的入口類HelloJVM
 5  * 找到HelloJVM會直接讀取該文件中的二進制數據,而且把該類的信息放到運行時的Method內存區域中。
 6  * 而後會定位到HelloJVM中的main方法的字節碼中,並開始執行Main方法中的指令
 7  * 此時會建立Student實例對象,而且使用student來引用該對象(或者說給該對象命名),其內幕以下:
 8  * 第一步:JVM會直接到Method區域中去查找Student類的信息,此時發現沒有Student類,就經過類加載器加載該Student類文件;
 9  * 第二步:在JVM的Method區域中加載並找到了Student類以後會在Heap區域中爲Student實例對象分配內存,
10  *         而且在Student的實例對象中持有指向方法區域中的Student類的引用(內存地址);
11  * 第三步:JVM實例化完成後會在當前線程中爲Stack中的reference創建實際的應用關係,此時會賦值給student
12  * 接下來就是調用方法
13  * 在JVM中方法的調用必定是屬於線程的行爲,也就是說方法調用自己會發生在線程的方法調用棧:
14  * 線程的方法調用棧(Method Stack Frames),每個方法的調用就是方法調用棧中的一個Frame,
15  * 該Frame包含了方法的參數,局部變量,臨時數據等 student.sayHello();
16  */
17 public class HelloJVM {
18     //在JVM運行的時候會經過反射的方式到Method區域找到入口方法main
19     public static void main(String[] args) {//main方法也是放在Method方法區域中的
20         /**
21          * student(小寫的)是放在主線程中的Stack區域中的
22          * Student對象實例是放在全部線程共享的Heap區域中的
23          */
24         Student student = new Student("spark");
25         /**
26          * 首先會經過student指針(或句柄)(指針就直接指向堆中的對象,句柄代表有一箇中間的,student指向句柄,句柄指向對象)
27          * 找Student對象,當找到該對象後會經過對象內部指向方法區域中的指針來調用具體的方法去執行任務
28          */
29         student.sayHello();
30     }
31 }
32 class Student {
33     // name自己做爲成員是放在stack區域的可是name指向的String對象是放在Heap中
34     private String name;
35     public Student(String name) {
36         this.name = name;
37     }
38     //sayHello這個方法是放在方法區中的
39     public void sayHello() {
40         System.out.println("Hello, this is " + this.name);
41     }
42 }
View Code

 

對象的訪問定位

java程序須要經過引用(ref)數據來操做堆上面的對象,那麼如何經過引用定位、訪問到對象的具體位置。

對象的訪問方式由虛擬機決定,java虛擬機提供兩種主流的方式

  • 句柄訪問對象
  • 直接指針訪問對象。(Sun HotSpot使用這種方式)

句柄訪問優勢:引用中存儲的是穩定的句柄地址,在對象被移動【垃圾收集時移動對象是常態】只需改變句柄中實例數據的指針,不須要改動引用【ref】自己。

直接指針訪問優勢:優點很明顯,就是速度快,相比於句柄訪問少了一次指針定位的開銷時間。【多是出於Java中對象的訪問時十分頻繁的,平時咱們經常使用的JVM HotSpot採用此種方式】

圖5 經過句柄訪問對象

圖6 經過指針訪問對象

 

2 classloader加載class文件的原理和機制


  返回

2.1 Classloader 類結構分析

主要由四個方法,分別是 defineClass , findClass , loadClass , resolveClass

  • Class defineClass(String name,byte[] b,int len):將類文件的字節數組轉換成JVM內部的java.lang.Class對象。字節數組能夠從本地文件系統、遠程網絡獲取。參數name爲字節數組對應的全限定類名。

  • Class findClass(String name),經過類名去加載對應的Class對象。當咱們實現自定義的classLoader一般是重寫這個方法,根據傳入的類名找到對應字節碼的文件,並經過調用defineClass解析出Class獨享

  • Class loadClass(String name) :name參數指定類裝載器須要裝載類的名字,必須使用全限定類名,如:com.smart.bean.Car。該方法有一個重載方法 loadClass(String name,boolean resolve),resolve參數告訴類裝載器時候須要解析該類,在初始化以前,因考慮進行類解析的工做,但並非全部的類都須要解析。若是JVM只須要知道該類是否存在或找出該類的超類,那麼就不須要進行解析。

  • resolveClass手動調用這個使得被加到JVM的類被連接(解析resolve這個類?)

實現自定義 ClassLoader 通常會繼承 URLClassLoader 類,由於這個類實現了大部分方法。

2.2 實現類的熱部署


 

  返回

  • 同一個classLoader的兩個實例加載同一個類,JVM也會識別爲兩個
  • 不能重複加載同一個類(全名相同,並使用同一個類加載器),會報錯
  • 不該該動態加載類,由於對象被引用後,對象的屬性結構被修改會引起問題

注意:使用不一樣classLoader加載的同一個類文件獲得的類,JVM將看成是兩個不一樣類,使用單例模式,強制類型轉換時均可能由於這個緣由出問題。

2.3 類加載器的雙親委派模型


  返回

圖3 類加載器雙親委派模型

類加載器雙親委派模型加載順序:java的三種類加載器存在父子關係,子 加載器保存着附加在其的引用,當一個類加載器須要加載一個目標類時,會先委託父加載器去加載,而後父加載器會在本身的加載路徑中搜索目標類,父加載器在本身的加載範圍中找不到時,纔會交給子加載器加載目標類。

採用雙親委託模式能夠避免類加載混亂,並且還將類分層次了,例如java中lang包下的類在jvm啓動時就被啓動類加載器加載了,而用戶一些代碼類則由應用程序類加載器(AppClassLoader)加載,基於雙親委託模式,就算用戶定義了與lang包中同樣的類,最終仍是由應用程序類加載器委託給啓動類加載器去加載,這個時候啓動類加載器發現已經加載過了lang包下的類了,因此二者都不會再從新加載。固然,若是使用者經過自定義的類加載器能夠強行打破這種雙親委託模型,但也不會成功的,java安全管理器拋出將會拋出java.lang.SecurityException異常。 

2.4 類加載的三種方式


  返回

  • 經過命令行啓動應用時由JVM初始化加載含有main()方法的主類。
  • 經過Class.forName()方法動態加載,會默認執行初始化塊(static{}),可是Class.forName(name,initialize,loader)中的initialze可指定是否要執行初始化塊。
  • 經過ClassLoader.loadClass()方法動態加載,不會執行初始化塊。
//1 由new關鍵字建立一個類的實例,在由運行時刻用 new 方法載入 
Person person = new Person();
//2 使用Class.forName()    經過反射加載類型,並建立對象實例
Class clazz = Class.forName("Person");
Object person =clazz.newInstance();
//3 使用某個ClassLoader實例的loadClass()方法,經過該 ClassLoader 實例的 loadClass() 方法載入。應用程序能夠經過繼承 ClassLoader 實現本身的類裝載器。
Class clazz = classLoader.loadClass("Person");
Object person =clazz.newInstance();

 

其中:

  • 1和2使用的類加載器是相同的,都是當前類加載器(即:this.getClass.getClassLoader)。
  • 3由用戶指定類加載器。若是須要在當前類路徑之外尋找類,則只能採用第3種方式。即第3種方式加載的類與當前類分屬不一樣的命名空間。
  • 1是靜態加載,二、3是動態加載
  • Class.forName(className)方法,內部實際調用的方法是 Class.forName(className,true,classloader);

    第2個boolean參數表示類是否須要初始化, Class.forName(className)默認是須要初始化。

    一旦初始化,就會觸發目標對象的 static塊代碼執行,static參數也也會被再次初始化。

  • ClassLoader.loadClass(className)方法,內部實際調用的方法是 ClassLoader.loadClass(className,false);

    第2個 boolean參數,表示目標對象是否進行連接,false表示不進行連接,由上面介紹能夠,

    不進行連接意味着不進行包括初始化等一些列步驟,那麼靜態塊和靜態對象就不會獲得執行

2.5  自定義類加載器的兩種方式


  返回

  • 遵照雙親委派模型:繼承ClassLoader,重寫findClass()方法。
  • 破壞雙親委派模型:繼承ClassLoader,重寫loadClass()方法。

一般咱們推薦採用第一種方法自定義類加載器,最大程度上的遵照雙親委派模型。 自定義類加載的目的是想要手動控制類的加載,那除了經過自定義的類加載器來手動加載類這種方式,還有其餘的方式麼?

利用現成的類加載器進行加載:

  • 利用當前類加載器
    Class.forName();
  • 經過系統類加載器
    Classloader.getSystemClassLoader().loadClass();
  • 經過上下文類加載器
    Thread.currentThread().getContextClassLoader().loadClass(); 

 

參考

[1] classloader加載class文件的原理和機制

[2] java 類加載器雙親委派模型

[3] 深刻理解JVM-內存模型(jmm)和GC

[4] 【深刻Java虛擬機】之二:Class類文件結構

相關文章
相關標籤/搜索