JVM細節版架構圖
本文針對Class Loader SubSystem這一塊展開講解類加載子系統的工作流程
類加載子系統作用
1.類加載子系統負責從文件系統或者網絡中加載class文件,class文件在文件開頭有特定的文件標識即16進制CA FE BA BE;
類加載子系統功能細分
加載模塊
1.通過一個類的全限定明獲取定義此類的二進制字節流;
2.將這個字節流所代表的的靜態存儲結構轉化為方法區的運行時數據;
3.在內存中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種數據的訪問入口
鏈接模塊分為三塊,即驗證、准備、解析
驗證
1.目的在於確保Class文件的字節流中包含信息符合當前虛擬機要求,保證被加載類的正確性,不會危害虛擬機自身安全。
2.主要包括四種驗證,文件格式驗證,源數據驗證,字節碼驗證,符號引用驗證。
准備
1.為類變量分配內存並且設置該類變量的默認初始值,即零值;
2.這里不包含用final修飾的sttic,因為final在編譯的時候就會分配了,准備階段會顯式初始化;
3.之類不會為實例變量分配初始化,類變量會分配在方法去中,而實例變量是會隨着對象一起分配到java堆中。
解析
1.將常量池內的符號引用轉換為直接引用的過程。
2.事實上,解析操作網晚會伴隨着jvm在執行完初始化之后再執行
3.符號引用就是一組符號來描述所引用的目標。符號應用的字面量形式明確定義在《java虛擬機規范》的class文件格式中。直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄
4.解析動作主要針對類或接口、字段、類方法、接口方法、方法類型等。對應常量池中的CONSTANT_Class_info/CONSTANT_Fieldref_info、CONSTANT_Methodref_info等。
初始化模塊,初始化階段就是執行類構造器方法clinit()的過程
1.clinit()即“class or interface initialization method”,注意他並不是指構造器init()
2.此方法不需要定義,是javac編譯器自動收集類中的所有類變量的賦值動作和靜態代碼塊中的語句合並而來。
3.我們注意到如果沒有靜態變量c,那么字節碼文件中就不會有clinit方法

構造器方法clinit()中指令按語句在源文件中出現的順序執行
虛擬機必須保證一個類的clinit()方法在多線程下被同步加鎖
即一個類只需被clinit一次,之后該類的內部信息就被存儲在方法區。
可以看到線程2並不會重復執行初始化操作
類加載器分類
1.JVM支持兩種類型的加載器,分別為引導類加載器C/C++實現(BootStrap ClassLoader)和自定義類加載器由Java實現
2.從概念上來講,自定義類加載器一般指的是程序中由開發人員自定義的一類類加載器,但是java虛擬機規范卻沒有這么定義,而是將所有派生於抽象類ClassLoader的類加載器都划分為自定義類加載器。
3.注意上圖中的加載器划分關系為包含關系,並不是繼承關系
4.按照這樣的加載器的類型划分,在程序中我們最常見的類加載器是:引導類加載器BootStrapClassLoader、自定義類加載器(Extension Class Loader、System Class Loader、User-Defined ClassLoader)
自定義類與核心類庫的加載器
1.對於用戶自定義類來說:將使用系統類System Class Loader加載器中的AppClassLoader進行加載
2.java核心類庫都是使用引導類加載器BootStrapClassLoader加載的
/** * ClassLoader加載 */ public class ClassLoaderTest { public static void main(String[] args) { //獲取系統類加載器 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 //獲取其上層 擴展類加載器 ClassLoader extClassLoader = systemClassLoader.getParent(); System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@610455d6 //獲取其上層 獲取不到引導類加載器 ClassLoader bootStrapClassLoader = extClassLoader.getParent(); System.out.println(bootStrapClassLoader);//null //對於用戶自定義類來說:使用系統類加載器進行加載 ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2 //String 類使用引導類加載器進行加載的 -->java核心類庫都是使用引導類加載器加載的 ClassLoader classLoader1 = String.class.getClassLoader(); System.out.println(classLoader1);//null獲取不到間接證明了String 類使用引導類加載器進行加載的 } }
虛擬機自帶的加載器
啟動類加載器(引導類加載器,BootStrap ClassLoader)
1.這個類加載使用C/C++語言實現的,嵌套在JVM內部
2.它用來加載java的核心庫(JAVA_HOME/jre/lib/rt.jar/resources.jar或sun.boot.class.path路徑下的內容),用於提供JVM自身需要的類
3.並不繼承自java.lang.ClassLoader,沒有父加載器
4.加載拓展類和應用程序類加載器,並指定為他們的父加載器,即ClassLoader
5.出於安全考慮,BootStrap啟動類加載器只加載包名為java、javax、sun等開頭的類
拓展類加載器(Extension ClassLoader)
1.java語言編寫 ,由sun.misc.Launcher$ExtClassLoader實現。
2.派生於ClassLoader類
3.從java.ext.dirs系統屬性所指定的目錄中加載類庫,或從JDK的安裝目錄的jre/lib/ext子目錄(擴展目錄)下加載類庫。如果用戶創建的JAR放在此目錄下,也會由拓展類加載器自動加載
應用程序類加載器(系統類加載器,AppClassLoader)
1.java語言編寫, 由sun.misc.Launcher$AppClassLoader實現。
2.派生於ClassLoader類
3.它負責加載環境變量classpath或系統屬性 java.class.path指定路徑下的類庫
4.該類加載器是程序中默認的類加載器,一般來說,java應用的類都是由它來完成加載
5.通過ClassLoader#getSystemClassLoader()方法可以獲取到該類加載器
代碼演示
/** * 虛擬機自帶加載器 */ 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);//sun.misc.Launcher$ExtClassLoader@4dc63996 } }
知識擴展:啟動類加載器BootStrapClassLoader能夠加載的api路徑有
最近看java.util.concurrent包的內容,發現java.time.、java.util.、java.nio.、java.lang.、java.text.、java.sql.、java.math.*等等都在rt.jar包下,
為什么要使用用戶自定義類加載器
1.隔離加載類
2.修改類加載的方式
3.拓展加載源
4.防止源碼泄漏
ClassLoader的常用方法及獲取方法
ClassLoader類,它是一個抽象類,其后所有的類加載器都繼承自ClassLoader(不包括啟動類加載器)
ClassLoader繼承關系
代碼示例如下
雙親委派機制
Java虛擬機對class文件采用的是按需加載的方式,也就是說當需要使用該類時才會將她的class文件加載到內存生成的class對象。而且加載某個類的class文件時,java虛擬機采用的是雙親委派模式,即把請求交由父類處理,它是一種任務委派 模式
雙親委派機制工作原理
代碼示例
如圖,雖然我們自定義了一個java.lang包下的String嘗試覆蓋核心類庫中的String,但是由於雙親委派機制,啟動加載器會加載java核心類庫的String類(BootStrap啟動類加載器只加載包名為java、javax、sun等開頭的類),而核心類庫中的String並沒有main方法
雙親委派機制的優勢
1.避免類的重復加載,如上
2.保護程序安全,防止核心API被隨意修改
啟動類加載器可以搶在標准擴展類裝載器之前去裝載類,而標准擴展類裝載器可以搶在類路徑加載器之前去裝載那個類,類路徑裝載 器又可以搶在自定義類加載器之前去加載它。所以Java虛擬機先從最可信的Java核心API查找類型,這是為了防止不可靠的類扮演被信任的類,試想一 下,網絡上有個名叫java.lang.Integer的類,它是某個黑客為了想混進java.lang包所起的名字,實際上里面含有惡意代碼,但是這種 伎倆在雙親模式加載體系結構下是行不通的,因為網絡類加載器在加載它的時候,它首先調用雙親類加載器,這樣一直向上委托,直到啟動類加載器,而啟動類加載 器在核心Java API里發現了這個名字的類,所以它就直接加載Java核心API的java.lang.Integer類,然后將這個類返回,所以自始自終網絡上的 java.lang.Integer的類是不會被加載的。
3.保證核心API包的訪問權限
但是如果這個移動代碼不是去試圖替換一個被信任的類(就是前面說的那種情況),而是想在一個被信任的包中插入一個全新的類型,情況會怎樣呢?比如一個名為 java.lang.Virus的類,經過雙親委托模式,最終類裝載器試圖從網絡上下載這個類,因為網絡類裝載器的雙親們都沒有這個類(當然沒有了,因為 是病毒嘛)。假設成功下載了這個類,那你肯定會想,Virus和lang下的其他類痛在java.lang包下,暗示這個類是Java API的一部分,那么是不是也擁有修改Java.lang包中數據的權限呢?答案當然不是,因為要取得訪問和修改java.lang包中的權 限,java.lang.Virus和java.lang下其他類必須是屬於同一個運行時包的,什么是運行時包?運行時包是指由同一個類裝載器裝載的、屬 於同一個包的、多個類型的集合。考慮一下,java.lang.Virus和java.lang其他類是同一個類裝載器裝載的嗎?不是 的!java.lang.Virus是由網絡類裝載器裝載的!
自定義類:java.lang.MeDsh(java.lang包需要訪問權限,阻止我們用包名自定義類)
雙親委派機制在SPI中的應用
1.某個應用程序由雙親委派機制找到引導類加載器,首先調用rt.jar包中的SPI核心,但由於SPI核心當中有各種各樣的接口需要被實現(這里指具體的服務提供商),這里我們已JDBC.jar為例,jdbc.jar可以為我們提供具體的實現。
2.那么這時我們需要反向委托,找到線程上下文類加載器去加載jdbc.jar
3.線程上下文類加載器屬於系統類加載器
JVM中表示兩個class對象是否為同一個類
1.在jvm中表示兩個class對象是否為同一個類存在的兩個必要條件
類的完整類名必須一致,包括包名
即使類的完整類名一致,同時要求加載這個類的ClassLoader(指ClassLoader實例對象)必須相同;是引導類加載器、還是定義類加載器
2.換句話說,在jvm中,即使這兩個類對象(class對象)來源同一個Class文件,被同一個虛擬機所加載,但只要加載它們的ClassLoader實例對象不同,那么這兩個類對象也是不相等的.
3.對類加載器的引用,JVM必須知道一個類型是有啟動類加載器加載的還是由用戶類加載器加載的。如果一個類型由用戶類加載器加載的,那么jvm會將這個類加載器的一個引用作為類型信息的一部分保存在方法區中。當解析一個類型到另一個類型的引用的時候,JVM需要保證兩個類型的加載器是相同的。
類的主動使用和被動使用
java程序對類的使用方式分為:主動使用和被動使用,即是否調用了clinit()方法
主動使用在類加載系統中的第三階段initialization即初始化階段調用了clinit()方法
而被動使用不會去調用
主動使用,分為七種情況
1.創建類的實例
2.訪問某各類或接口的靜態變量,或者對靜態變量賦值
3.調用類的靜態方法
4.反射 比如Class.forName(com.dsh.jvm.xxx)
5.初始化一個類的子類
6.java虛擬機啟動時被標明為啟動類的類
7.JDK 7 開始提供的動態語言支持:java.lang.invoke.MethodHandle實例的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic句柄對應的類沒有初始化,則初始化除了以上七種情況,其他使用java類的方式都被看作是對類的被動使用,都不會導致類的初始化。