JVM詳解(二)類加載子系統
在這裏感謝尚硅谷JVM(宋紅康),在此記錄一下自己詳細對學習筆記,希望對你有所幫助。
02類加載子系統
如果自己手寫一一個Java虛擬機的話,主要考慮哪些結構呢?
類加載器 和 執行引擎
類加載器與類的加載過程
對於第三點常量池
常量池中有具體的符號引用
在運行時加載到內存裏,就叫做運行時常量池
- 編譯後到Car class文件放在硬盤上
- 物理磁盤中的文件通過二進制流的方式加載到內存
- 通過類加載器存放在方法區中(即DNA元數據模版)
- 通過getClassLoader()獲取是誰加載的這個類(即獲得了這個類的類加載器)
- 在內存當中調用這個Car 這個class的類構造器就可以在堆中創建幾個對象
- 針對具體的對象也可調用getClass可以獲取類本身(即你是由哪個類所創建的對象)
一個簡單的例子
自定義類使用的是系統類加載器,如果加載的過程中不是一個合法的字節碼文件,會拋出異常
加載:
1.通過一個類的全限定名獲取定義此類的二進制字芹流
2.將這個字節流所代表的靜態存儲結構轉化爲方法區(7以前叫永久代,以後叫元數據,理解爲存的模版,有需要根據這個模版在堆中生成對象,和spring容器差不多)的運行時數據結構
3.在內存中生成一個代表這個類的java. lang.Class對象(生成Class實例在這個階段),作爲方法區這個類
的各種數據的訪問入口
補充:加載. class文件的方式
- 從本地系統中直接加載
- 通過網絡獲取,典型場景: Web Applet
- 從zip壓縮包中讀取,成爲日後jar、war格式的基礎
- 運行時計算生成,使用最多的是:動態代理技術
- 由其他文件生成,典型場景: JSP應用
- 從專有數據庫中提取. class文件,比較少見
- 從加密文件中獲取,典型的防Class文件被反編譯的保護措施
類的加載過程
驗證(Verify) :
目的在於確保Class文件的字節流中包含信息符合當前虛擬機要求,保證被加載類的正確性,不會危害虛擬機自身安全。
主要包括四種驗證,文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證。
準備(Prepare) :
爲類變量分配內存並且設置該類變量的默認初始值,即零值。
這裏不包含用final修飾的static,因爲final在編譯的時候就會分配了,準備階段會顯式初始化;
這裏不會爲實例變量分配初始化,類變量會分配在方法區中,而實例變量是會隨着對象一起分配到Java堆中。
解析(Resolve) :
- 將常量池內的符號引用轉換爲直接引用的過程。
- 事實上,解析操作往往會伴隨着JVM在執行完初始化之後再執行。
- 符號引用就是一組符號來描述所引用的目標。符號引用的字面量形式明確定義在《java虛擬機規範》的Class文件格式中。直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。
- 解析動作主要針對類或接口、字段、類方法、接口方法、方法類型等。對應常量池中的CONSTANT Class info、 CONSTANT Fieldref info、 CONSTANT Methodref info等 。
打開字節碼文件查看二進制
字節碼啓始爲CAFE BABE代表JAVA虛擬機的特定標識,驗證過程
反編譯後,常量池中會加載這些許多的類
解析環境會轉爲直接引用
初始化:
- 初始化階段就是執行類構造器方法()的過程。
- 此方法不需定義,是javac編譯器自動收集類中的所有類變量的賦值動作和靜
態代碼塊中的語句合併而來。 - 構造器方法中指令按語句在源文件中出現的順序執行。
- ()不同於類的構造器。(關聯: 構造器是虛擬機視角下的() )
- 若該類具有父類,JVM會 保證子類的()執行前,父類的 ()
已經執行完畢。 - 虛擬機必須保證-一個類的()方法在多線程下被同步加鎖。
注意
可以賦值,若前面沒有聲明不能引用
因爲變量在準備階段就已經被分配到方法區中,此時具有零值(默認值),賦與類變量值是在初始化階段,在準備所屬的鏈接階段之後
若沒有類變量與靜態代碼塊,則不會存在
此時就有了
構造器方法
一個類的()方法在多線程下被同步加鎖
注意,若業務中出現此情況,會將其他線程變成阻塞狀態
類的加載器
- JVM支持兩種類型的類加載器,分別爲引導類加載器(Bootstrap
**ClassLoader) **和 自定義類加載器(User-Defined ClassLoader) - 從概念上來講,自定義類加載器一-般指的是程序中由開發人員自定義的一類
類加載器,但是Java虛擬機規範卻沒有這麼定義,而是將所有派生於抽象類
ClassLoader的類加載器都劃分爲自定義類加載器 - 無論類加載器的類型如何劃分,在程序中我們最常見的類加載器始終只有3
個,如下所示:
Extention Class Loader 與 System Class Loader都間接繼承了ClassLoader,所以他們也被稱爲自定義加載器
- Class類中
@CallerSensitive
public ClassLoader getClassLoader() {
ClassLoader cl = getClassLoader0();
if (cl == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
}
return cl;
}
虛擬機自帶的加載器
啓動類加載器(引導類加載器,Bootstrap ClassLoader)
➢這個類加載使用C/C++語言實現的,嵌套在JVM內部(即JVM的一部分)。
➢它用來加載Java的核心庫(JAVA HOME/jre/lib/rt. jar、resources.jar或sun . boot.class .path路徑下的內容) ,用於提供JVM自身需要的類
➢並不繼承自java. lang.ClassLoader,沒有父加載器。
➢加載擴展類和應用程序類加載器,並指定爲他們的父類加載器。
➢出於安全考慮,Bootstrap啓動類加載器只加載包名爲java、javax、sun等開頭的類
擴展類加載器( Extension ClassLoader)
➢Java語言編寫,由sun . misc. Launcher$ExtClassLoader實現(Launcher的一個內部類,代碼有體現)。
➢派生於ClassLoader類
➢父類加載器爲啓動類加載器:
➢從java.ext. dirs系統屬性所指定的目錄中加載類庫,或從JDK的安裝目錄的jre/lib/ext子目錄(擴展目錄)下加載類庫。如果用戶創建的JAR放在此目錄下,也會自動由擴展類加載器加載。
Extension ClassLoader爲Launcher的一個內部類
應用程序類加載器(系統類加載器,AppClassLoader )
➢java語言編寫,由sun. misc. Launcher$AppClassLoader實現
➢派生於ClassLoader類
➢父類加載器爲擴展類加載器
➢它負責加載環境變量classpath或系統屬性java.class.path 指定路徑下的類庫
➢該類加載是程序中默認的類加載器,一般來說,Java應用的類都是由它來完成加載
➢通過ClassLoader#getSystemClassLoader ()方法可以獲取到該類加載器
ClassLoader1
結果:
同理:
用戶自定義類加載器
●在Java的日常應用程序開發中,類的加載幾乎是由上述3種類加載器相互配合執行的,在必要時,我們還可以自定義類加載器,來定製類的加載方式。
●爲什麼要自定義類加載器?
➢隔離加載類
➢修改類加載的方式
➢擴展加載源
➢防止源碼泄漏
隔離加載類
由於中間件都有自己的依賴的大包,然後在同一個工程裏邊如果引入多個框架的話有可能會出現比如某些類路徑一樣、類型也相同,那在這種情況下呢就會出類的衝突,我們就需要做一個類的仲裁。像主流的容器類的框架,他們都會自定義這個類加載器。
修改類加載方式
在整個類的加載中,bootstrap是一定會使用的,因爲他一定會加載系統核心的api。其他類可能就是不是必須的,在實際情況中,可以在要用的時候再引入。在需要的時候進行一個動態的加載。
擴展加載源
除了前面提到了加載的類可以比如有本地的物理磁盤,通過網絡中,通過jar包中等等去加載之外,還可以考慮像比如說數據庫當中,甚至說這個電視機的機頂盒等等,我們去加載這個字節碼文件的來源,所以通過自定義類加載器,可以來擴展加載來源。
防止源碼泄漏
Java代碼實際上是很容易被編譯和篡改的,有了這個字節碼文件以後,沒有這個反編譯的一些手段的話很容易的就被反編譯了,容易被篡改。爲了防止被編譯和篡改,對這個字節碼文件來進行加密,你自己在運行的時候把它再還原成內存中的這個類去執行的時候,我們需要解密,那這個時候呢,我們可以去自定義類加載器去實現這樣的一個解密操作。
用戶自定義類加載器實現步驟:
1.開發人員可以通過繼承抽象類java. lang. ClassLoader類的方式, 實現自己的類加載器,以滿足一些特殊的需求
2.在JDK1.2之前, 在自定義類加載器時,總會去繼承ClassLoader類並重寫loadClass()方法,從而實現自定義的類加載類,但是在JDK1.2之後已不再建議用戶去覆蓋loadClass()方法,而是建議把自定義的類加載邏輯寫在findClass()方法中
3.在編寫自定義類加載器時,如果沒有太過於複雜的需求,可以直接繼承URLClassLoader類,這樣就可以避免自己去編寫findClass()方法及其獲取字節碼流的方式
如沒有一些解密操作
使自定義類加載器編寫更加簡潔。
/**
* @author Aaron
* @description 自定義用戶類加載器
* @date 2020/5/2 9:48 AM
*/
public class CustomClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
//根據路徑name,以二進制流的方式讀到內存裏面,形成一個字節數組
byte[] result = getClassFromCustomPath(name);
if (result == null) {
throw new FileNotFoundException(name);
} else {
// defineClass方法將字節碼轉化爲類
return defineClass(name, result, 0, result.length);
}
} catch (Exception e) {
e.printStackTrace();
}
throw new ClassNotFoundException(name);
}
private byte[] getClassFromCustomPath(String name) {
// 從自定義路徑中加載指定類,返回類的字節碼文件
// 如果指定路徑的字節碼文件進行了加密,需要在此方法中解密操作
InputStream in = null;
ByteArrayOutputStream out = null;
String path = "/Users/john/" + name + ".class";
try {
in = new FileInputStream(path);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int len = 0;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
return out.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
in.close();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
public static void main(String[] args) {
CustomClassLoader customClassLoader = new CustomClassLoader();
try {
Class<?> clazz = Class.forName("One", true, customClassLoader);
Object obj = clazz.newInstance();
// cn.xpleaf.coding.c4.CustomClassLoader@610455d6
System.out.println(obj.getClass().getClassLoader());
} catch (Exception e) {
e.printStackTrace();
}
}
}
ClassLoader類,它是一個抽象類,其後所有的類加載器都繼承自
ClassLoader ( 不包括啓動類加載器)
方法名稱 | 描述 |
---|---|
getParent() | 返回該類加載器的超累加載器 |
loadClass(String name) | 加載名稱爲name的類,返回結果爲java.lang.Class類的實例 |
findLoadedClass(String name) | 查找名稱爲name的已經被加載過的類,返回結果爲java.lang.Class類的實例 |
defineClass(String name,byte[] b,int off, int len) | 把字節數組b中的內容轉換爲一個Java類,返回結果爲java.lang.Class類的實例 |
resolveClass(Class<?> c) | 連接指定的一個Java類 |
方式一:獲取當前類的ClassLoader |
---|
class.getClassLoader() |
方式二:獲取當前線程上下文的ClassLoader |
Thread.currentThread().getContextClassLoader() |
方式三:獲取系統的ClassLoader |
ClassLoader.getSystemClassLoader() |
方式四:獲取調用者的ClassLoader |
DriverManager.getCallerClassLoader() |
public class ClassLoaderTest2 {
public static void main(String[] args) throws ClassNotFoundException {
//1
ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
System.out.println(classLoader); //null
//2 通過線程獲取該上下文的一個加載器,上下文在自定義的這個程序當中
ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
System.out.println(classLoader1); //sun.misc.Launcher$AppClassLoader@18b4aac2
//3
ClassLoader classLoader2 = ClassLoader.getSystemClassLoader().getParent();
System.out.println(classLoader2);//sun.misc.Launcher$ExtClassLoader@610455d6
}
}
雙親委派機制
Java虛擬機對class文件採用的是按需加載的方式,也就是說當需要使用該
類時纔會將它的class文件加載到內存生成class對象。而且加載某個類的
class文件時,Java虛擬機採用的是雙親委派模式,即把請求交由父類處理,
它是-種任務委派模式。
靜態代碼塊在第三個階段(初始化)被調用,把靜態代碼塊,靜態變量顯示賦值放在中
從結果來看不是執行的自定義實現的String。爲了這種防止,引入雙親委派機制
所以String不會由AppClassLoader加載,會由引導類加載器加載
再舉一個例子:
- 想要去執行main方法,則main方法所在的類需要被加載
- 爲這個String,委託給BootstrapLoader。它就加載了核心API的java.lang中的String,但是沒有main方法,所以報錯
再舉一個例子
他這個雙親委派針對於包名,類名相同的情況下,加如我設置包名不同(自己定義的類),它爲AppClassLoader
下面對以上流程作出一個解釋:
我們在程序中需要用到SPI接口,它屬於這個核心API。那我們就使用雙親委派機制,**依次 **到引導類加載器,然後到引導類加載器去加載rt.jar。SPI的核心類就加載過來了。那麼這裏邊會存在一些interface接口,那接口呢,需要用一些具體的實現類了,那具體實現類呢,這就涉及到一些第三方的jar包了,我們要加載的是JDBC的jar包,那我們要加載第三方的時候呢,這個時候因爲你是第三方的不屬於核心的API,其實就應該是由我們所謂的AppClassLoader加載,所以這就出現一個叫 反向委派 ,一直這樣委派就委派到AppClassLoader,這塊兒實際上是由我們當前線程的,通過getContextClassLoader獲取到的,然後由他來加載我們SPI接口的具體實現類,jdbc.jar包裏邊兒的這些API,所以這裏邊兒我們就會看到接口是由Bootstrap ClassLoader加載的,而具體接口的實現類,是第三方的,是由我們這個ContextClassLoader加載的,而ContextClassLoader呢,就是我們的AppClassLoader,在前邊獲取類加載器的時候,演示過通過線程來獲取,一般情況下我們拿到的都是一個AppClassLoader去加載我們第三方的jdbc.jar包下的API,這是這個圖想說明的。
雙親委派優勢
➢避免類的重複加載 即加載有一個層次關係
如:BootstrapClassLoader->ExtClassLoader->AppClassLoader,上面有圖
➢保護程序安全,防止核心API被隨意篡改
自定義類: java.1ang. String
自定義類: java. lang. ShkStart
java. lang. SecurityException:Prohibited package name: java.lang
舉例:
阻止報名爲java.lang包。我們按照雙親委派機制依次往上,Bootstrap ClassLoader發現爲java開頭,則發現爲自己管的,它就去加載這個類。java.lang包的訪問需要權限,java.lang.SecurityException,阻止我們用這個報名定義我們的自定義類。你可能會問這個跟之前String區別,我的理解是:因爲存在這個機制,String本來就會在Bootstrap ClassLoader中加載成功,所以String不會影響。但是若Aaron加載成功(本來引導類加載器它自身沒有這個東西),它可能就會懷疑是惡意的,會對它自己有影響。
在雙親委派機制中還有一個 “沙箱安全機制”。
在這只是介紹下這個名字,前面例子已說明
自定義String類,但是在加載自定義String類的時候會率先使用引導類加載器加載,而引導類加載器在加載的過程中會先如載jdk自帶的文件) (rt. jar包中java\lang\String. class),報錯信息說沒有main方法,就是因爲加載的是rt. jar包中的String類。這樣可以保證對java核心源代碼的保護,這就是沙箱安全機制。
其他補充內容
●在JVM中表示兩個class對象是否爲同一個類存在兩個必要條件:
➢類的完整類名必須一致, 包括包名。 前面javapp.langpp.String已說明
➢加載這個類的ClassLoader (指ClassLoader實例對象)必須相同。例如自定義的爲AppClassLoader,核心中的String爲Bootstrap ClassLoader,前面例子也說明了,跟上一個例子相同
●換句話說,在JVM中,即使這兩個類對象(class對象)來源同一個Class文件,被同一個虛擬機所加載,但只要加載它們的ClassLoader實例對象不同,那麼這兩個類對象也是不相等的。
對類加載器的引用
JVM必須知道一個類型是由啓動加載器加載的還是由用戶類加載器加載的。如果一個類型是由用戶類加載器加載的,那麼JVM會將這個類加載器的一個引用作爲類型信息的一部分保存在方法區中。當解析一個類型到另一個類型的引用的時候,JVM需要保證這兩個類型的類加載器是相同的
後面學習到動態鏈接再解釋
類的主動使用和被動使用
Java程序對類的使用方式分爲:主動使用和被動使用。
●主動使用,又分爲七種情況:
➢創建類的實例
➢訪問某個類或接口的靜態變量,或者對該靜態變量賦值
➢調用類的靜態方法
➢反射(比如: Class . forName (“top. p3wj. java.StringTest”) )
➢初始化一個類的子類 . 它的父類也會被初始化
➢Java虛擬機啓動時被標明爲啓動類的類
➢JDK 7開始提供的動態語言支持:
java. lang. invoke . MethodHandle實例的解析結果REF_getStatic、 REF_putStatic、 REF_invokeStatic句柄對應的類沒有初始化,則初始化
●除了以上七種情況,其他使用Java 類的方式都被看作是對類的被動使用,都不會導致類的初始化。
解釋:(類的加載過程:加載->鏈接[驗證、準備、解析]->初始化)
就是說當你要是被動使用的話會被加載,畢竟使用了,一旦使用了我們就需要加載到內存當中。但是呢,在初始化的時候不一定會去調用這個方法,這個方法會涉及到靜態的屬性靜態代碼塊的一個執行了,你要是靜態的屬性會有一個顯示賦值,尤其是這個靜態代碼塊是否執行要看是否執行過這個,所以呢,主動使用被動使用的區別就在於這個操作是否執行了。
之後再做驗證