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總體架構圖網絡
JVM被分爲三個主要的子系統:數據結構
返回架構
圖2 類加載器jvm
Java的動態類加載功能是由類加載器子系統處理。當它在運行時(不是編譯時)首次引用一個類時,它加載、連接並初始化該類文件。ide
加載器:類由此組件加載。啓動類加載器 (BootStrap class Loader)、擴展類加載器(Extension class Loader)和應用程序類加載器(Application class Loader) 這三種類加載器幫助完成類的加載。函數
加載過程:
校驗: 字節碼校驗器會校驗生成的字節碼是否正確,若是校驗失敗,咱們會獲得校驗錯誤。
準備:分配內存並初始化默認值給全部的靜態變量。
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的常量值等。而符號引用總結起來則包括了下面三類常量:
其中:
符號引用和直接引用的區別與關聯:
這是類加載的最後階段,這裏全部的靜態變量會被賦初始值, 而且靜態塊將被執行。
java中,對於初始化階段,有且只有如下五種狀況纔會對要求類馬上初始化:
圖4 運行時數據區
分配給運行時數據區的字節碼將由執行引擎執行。執行引擎讀取字節碼並逐段執行。
解釋器:解釋器能快速的解釋字節碼,但執行卻很慢。 解釋器的缺點就是,當一個方法被調用屢次,每次都須要從新解釋。
編譯器:JIT編譯器消除了解釋器的缺點。執行引擎利用解釋器轉換字節碼,但若是是重複的代碼則使用JIT編譯器將所有字節碼編譯成本機代碼。本機代碼將直接用於重複的方法調用,這提升了系統的性能。
垃圾回收器: 收集並刪除未引用的對象。能夠經過調用"System.gc()"來觸發垃圾回收,但並不保證會確實進行垃圾回收。JVM的垃圾回收只收集哪些由new關鍵字建立的對象。因此,若是不是用new建立的對象,你可使用finalize函數來執行清理。Java本地接口 (JNI): JNI會與本地方法庫進行交互並提供執行引擎所需的本地庫。本地方法庫:它是一個執行引擎所需的本地庫的集合。
經過如下代碼看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 }
對象的訪問定位
java程序須要經過引用(ref)數據來操做堆上面的對象,那麼如何經過引用定位、訪問到對象的具體位置。
對象的訪問方式由虛擬機決定,java虛擬機提供兩種主流的方式
句柄訪問優勢:引用中存儲的是穩定的句柄地址,在對象被移動【垃圾收集時移動對象是常態】只需改變句柄中實例數據的指針,不須要改動引用【ref】自己。
直接指針訪問優勢:優點很明顯,就是速度快,相比於句柄訪問少了一次指針定位的開銷時間。【多是出於Java中對象的訪問時十分頻繁的,平時咱們經常使用的JVM HotSpot採用此種方式】
圖5 經過句柄訪問對象
圖6 經過指針訪問對象
主要由四個方法,分別是 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加載的同一個類文件獲得的類,JVM將看成是兩個不一樣類,使用單例模式,強制類型轉換時均可能由於這個緣由出問題。
圖3 類加載器雙親委派模型
類加載器雙親委派模型加載順序:java的三種類加載器存在父子關係,子 加載器保存着附加在其的引用,當一個類加載器須要加載一個目標類時,會先委託父加載器去加載,而後父加載器會在本身的加載路徑中搜索目標類,父加載器在本身的加載範圍中找不到時,纔會交給子加載器加載目標類。
採用雙親委託模式能夠避免類加載混亂,並且還將類分層次了,例如java中lang包下的類在jvm啓動時就被啓動類加載器加載了,而用戶一些代碼類則由應用程序類加載器(AppClassLoader)加載,基於雙親委託模式,就算用戶定義了與lang包中同樣的類,最終仍是由應用程序類加載器委託給啓動類加載器去加載,這個時候啓動類加載器發現已經加載過了lang包下的類了,因此二者都不會再從新加載。固然,若是使用者經過自定義的類加載器能夠強行打破這種雙親委託模型,但也不會成功的,java安全管理器拋出將會拋出java.lang.SecurityException異常。
//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();
其中:
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表示不進行連接,由上面介紹能夠,
不進行連接意味着不進行包括初始化等一些列步驟,那麼靜態塊和靜態對象就不會獲得執行
一般咱們推薦採用第一種方法自定義類加載器,最大程度上的遵照雙親委派模型。 自定義類加載的目的是想要手動控制類的加載,那除了經過自定義的類加載器來手動加載類這種方式,還有其餘的方式麼?
利用現成的類加載器進行加載:
[1] classloader加載class文件的原理和機制
[2] java 類加載器雙親委派模型