學(xué)習(xí)類加載之前我們先看看從面試官的角度會問哪些問題?帶著問題學(xué)習(xí)會更加高效。
直擊面試
- 看你簡歷寫得熟悉 JVM,那你說說類的加載過程吧?
- 我們可以自定義一個 String 類來使用嗎?
- 什么是類加載器,類加載器有哪些?這些類加載器都加載哪些文件?
- 多線程的情況下,類的加載為什么不會出現(xiàn)重復(fù)加載的情況?
- 什么是雙親委派機(jī)制?它有啥優(yōu)勢?可以打破這種機(jī)制嗎?
類加載子系統(tǒng)
類加載機(jī)制概念
Java 虛擬機(jī)把描述類的數(shù)據(jù)從 Class 文件加載到內(nèi)存,并對數(shù)據(jù)進(jìn)行校驗、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的 Java 類型,這就是虛擬機(jī)的加載機(jī)制。Class 文件由類裝載器裝載后,在 JVM 中將形成一份描述 Class 結(jié)構(gòu)的元信息對象,通過該元信息對象可以獲知 Class 的結(jié)構(gòu)信息:如構(gòu)造函數(shù),屬性和方法等,Java 允許用戶借由這個 Class 相關(guān)的元信息對象間接調(diào)用 Class 對象的功能,這里就是我們經(jīng)常能見到的 Class 類。
類加載子系統(tǒng)作用
- 類加載子系統(tǒng)負(fù)責(zé)從文件系統(tǒng)或者網(wǎng)絡(luò)中加載 class 文件,class 文件在文件開頭有特定的文件標(biāo)識(0xCAFEBABE)
- ClassLoader 只負(fù)責(zé) class 文件的加載。至于它是否可以運行,則由 Execution Engine 決定
- 加載的類信息存放于一塊稱為方法區(qū)的內(nèi)存空間。除了類的信息外,方法區(qū)中還存放- 運行時常量池信息,可能還包括字
- Class 對象是存放在堆區(qū)的
類加載器 ClassLoader 角色
- class file 存在于本地硬盤上,可以理解為設(shè)計師畫在紙上的模板,而最終這個模板在執(zhí)行的時候是要加載到JVM 當(dāng)中來根據(jù)這個文件實例化出 n 個一模一樣的實例
- class file 加載到 JVM 中,被稱為 DNA 元數(shù)據(jù)模板,放在方法區(qū)
- 在 .calss 文件 -> JVM -> 最終成為元數(shù)據(jù)模板,此過程就要一個運輸工具(類裝載器),扮演一個快遞員的角色
類加載過程
類從被加載到虛擬機(jī)內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個生命周期包括:加載、驗證、準(zhǔn)備、解析、初始化、使用和卸載七個階段。(驗證、準(zhǔn)備和解析又統(tǒng)稱為連接,為了支持 Java 語言的運行時綁定,所以解析階段也可以是在初始化之后進(jìn)行的。以上順序都只是說開始的順序,實際過程中是交叉的混合式進(jìn)行的,加載過程中可能就已經(jīng)開始驗證了)
(資料圖片僅供參考)
1. 加載(Loading):
- 通過一個類的全限定名獲取定義此類的二進(jìn)制字節(jié)流
- 將這個字節(jié)流所代表的的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)
- 在內(nèi)存中生成一個代表這個類的 java.lang.Class 對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口
加載 .class 文件的方式
- 從本地系統(tǒng)中直接加載
- 通過網(wǎng)絡(luò)獲取,典型場景:Web Applet
- 從 zip 壓縮文件中讀取,成為日后 jar、war 格式的基礎(chǔ)
- 運行時計算生成,使用最多的是:動態(tài)代理技術(shù)
- 由其他文件生成,比如 JSP 應(yīng)用
- 從專有數(shù)據(jù)庫提取 .class 文件,比較少見
- 從加密文件中獲取,典型的防 Class 文件被反編譯的保護(hù)措施
2. 連接(Linking)
驗證(Verify)
目的在于確保 Class 文件的字節(jié)流中包含信息符合當(dāng)前虛擬機(jī)要求,保證被加載類的正確性,不會危害虛擬機(jī)自身安全
主要包括四種驗證,文件格式驗證,元數(shù)據(jù)驗證,字節(jié)碼驗證,符號引用驗證
準(zhǔn)備(Prepare)
- 為類變量分配內(nèi)存并且設(shè)置該類變量的默認(rèn)初始值,即零值
數(shù) 據(jù) 類 型 | 零 值 |
---|---|
int | 0 |
long | 0L |
short | (short)0 |
char | "\u0000" |
byte | (byte)0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
這里不包含用 final 修飾的 static,因為 final 在編譯的時候就會分配了,準(zhǔn)備階段會顯示初始化
這里不會為實例變量分配初始化,類變量會分配在方法區(qū)中,而實例變量是會隨著對象一起分配到 Java 堆中
private static int i = 1; //變量i在準(zhǔn)備階只會被賦值為0,初始化時才會被賦值為1private final static int j = 2; //這里被final修飾的變量j,直接成為常量,編譯時就會被分配為2
解析(Resolve)
- 將常量池內(nèi)的符號引用轉(zhuǎn)換為直接引用的過程
- 事實上,解析操作往往會伴隨著 JVM 在執(zhí)行完初始化之后再執(zhí)行
- 符號引用就是一組符號來描述所引用的目標(biāo)。符號引用的字面量形式明確定義在《Java虛擬機(jī)規(guī)范》的 Class文件格式中。直接引用就是直接指向目標(biāo)的指針、相對偏移量或一個間接定位到目標(biāo)的句柄
- 解析動作主要針對類或接口、字段、類方法、接口方法、方法類型等。對應(yīng)常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等
3. 初始化(Initialization)
- 初始化階段就是執(zhí)行類構(gòu)造器方法
() 的過程 - 此方法不需要定義,是 javac 編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)代碼塊中的語句合并而來
- 構(gòu)造器方法中指令按語句在源文件中出現(xiàn)的順序執(zhí)行
() 不同于類的構(gòu)造器(構(gòu)造器是虛擬機(jī)視角下的 ()) - 若該類具有父類,JVM 會保證子類的
() 執(zhí)行前,父類的 () 已經(jīng)執(zhí)行完畢 - 虛擬機(jī)必須保證一個類的
() 方法在多線程下被同步加鎖
public class ClassInitTest{ private static int num1 = 30; static{ num1 = 10; num2 = 10; //num2寫在定義變量之前,為什么不會報錯呢?? System.out.println(num2); //這裡直接打印可以嗎? 報錯,非法的前向引用,可以賦值,但不可調(diào)用 } private static int num2 = 20; //num2在準(zhǔn)備階段就被設(shè)置了默認(rèn)初始值0,初始化階段又將10改為20 public static void main(String[] args){ System.out.println(num1); //10 System.out.println(num2); //20 }}
類的主動使用和被動使用
Java 程序?qū)︻惖氖褂梅绞椒譃椋褐鲃邮褂煤捅粍邮褂谩L摂M機(jī)規(guī)范規(guī)定有且只有 5 種情況必須立即對類進(jìn)行“初始化”,即類的主動使用。
- 創(chuàng)建類的實例、訪問某個類或接口的靜態(tài)變量,或者對該靜態(tài)變量賦值、調(diào)用類的靜態(tài)方法(即遇到 new、getstatic、putstatic、invokestatic 這四條字節(jié)碼指令時)
- 反射
- 初始化一個類的子類
- Java 虛擬機(jī)啟動時被標(biāo)明為啟動類的類
- JDK7 開始提供的動態(tài)語言支持:java.lang.invoke.MethodHandle 實例的解析結(jié)果,REF_getStatic、REF_putStatic、REF_invokeStatic 句柄對應(yīng)的類沒有初始化,則初始化
除以上五種情況,其他使用 Java 類的方式被看作是對類的被動使用,都不會導(dǎo)致類的初始化。
例如:
public class NotInitialization { public static void main(String[] args) { //只輸出SupperClass int 123,不會輸出SubClass init //對于靜態(tài)字段,只有直接定義這個字段的類才會被初始化 System.out.println(SubClass.value); }}class SuperClass { static { System.out.println("SupperClass init"); } public static int value = 123;}class SubClass extends SuperClass { static { System.out.println("SubClass init"); }}
類加載器
JVM 支持兩種類型的類加載器,分別為引導(dǎo)類加載器(Bootstrap ClassLoader)和自定義類加載器(User-Defined ClassLoader)
從概念上來講,自定義類加載器一般指的是程序中由開發(fā)人員自定義的一類類加載器,但是 Java 虛擬機(jī)規(guī)范卻沒有這么定義,而是將所有派生于抽象類 ClassLoader 的類加載器都劃分為自定義類加載器
啟動類加載器(引導(dǎo)類加載器,Bootstrap ClassLoader)
- 這個類加載使用 C/C++ 語言實現(xiàn),嵌套在 JVM 內(nèi)部
- 它用來加載 Java 的核心庫(JAVA_HOME/jre/lib/rt.jar、resource.jar或sun.boot.class.path路徑下的內(nèi)容),用于提供 JVM 自身需要的類
- 并不繼承自 java.lang.ClassLoader,沒有父加載器
- 加載擴(kuò)展類和應(yīng)用程序類加載器,并指定為他們的父類加載器
- 出于安全考慮,Bootstrap 啟動類加載器只加載名為 java、Javax、sun 等開頭的類
擴(kuò)展類加載器(Extension ClassLoader)
- Java 語言編寫,由 sun.misc.Launcher$ExtClassLoader 實現(xiàn)
- 派生于 ClassLoader
- 父類加載器為啟動類加載器
- 從 java.ext.dirs 系統(tǒng)屬性所指定的目錄中加載類庫,或從 JDK 的安裝目錄的 jre/lib/ext 子目錄(擴(kuò)展目錄)下加載類庫。如果用戶創(chuàng)建的 JAR 放在此目錄下,也會自動由擴(kuò)展類加載器加載
應(yīng)用程序類加載器(也叫系統(tǒng)類加載器,AppClassLoader)
- Java 語言編寫,由 sun.misc.Lanucher$AppClassLoader 實現(xiàn)派生于 ClassLoader
- 父類加載器為擴(kuò)展類加載器
- 它負(fù)責(zé)加載環(huán)境變量 classpath 或系統(tǒng)屬性 java.class.path 指定路徑下的類庫
- 該類加載是程序中默認(rèn)的類加載器,一般來說,Java 應(yīng)用的類都是由它來完成加載的通過 ClassLoader#getSystemClassLoader() 方法可以獲取到該類加載器
public class ClassLoaderTest { public static void main(String[] args) { //獲取系統(tǒng)類加載器 ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); System.out.println(systemClassLoader); //sun.misc.Launcher$AppClassLoader@135fbaa4 //獲取其上層:擴(kuò)展類加載器 ClassLoader extClassLoader = systemClassLoader.getParent(); System.out.println(extClassLoader); //sun.misc.Launcher$ExtClassLoader@2503dbd3 //再獲取其上層:獲取不到引導(dǎo)類加載器 ClassLoader bootstrapClassLoader = extClassLoader.getParent(); System.out.println(bootstrapClassLoader); //null //對于用戶自定義類來說,默認(rèn)使用系統(tǒng)類加載器進(jìn)行加載,輸出和systemClassLoader一樣 ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); System.out.println(classLoader); //sun.misc.Launcher$AppClassLoader@135fbaa4 //String 類使用引導(dǎo)類加載器進(jìn)行加載。Java的核心類庫都使用引導(dǎo)類加載器進(jìn)行加載,所以也獲取不到 ClassLoader classLoader1 = String.class.getClassLoader(); System.out.println(classLoader1); //null //獲取BootstrapClassLoader可以加載的api的路徑 URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs(); for (URL url : urls) { System.out.println(url.toExternalForm()); } }}
用戶自定義類加載器
在 Java 的日常應(yīng)用程序開發(fā)中,類的加載幾乎是由 3 種類加載器相互配合執(zhí)行的,在必要時,我們還可以自定義類加載器,來定制類的加載方式
為什么要自定義類加載器?
- 隔離加載類
- 修改類加載的方式
- 擴(kuò)展加載源(可以從數(shù)據(jù)庫、云端等指定來源加載類)
- 防止源碼泄露(Java 代碼容易被反編譯,如果加密后,自定義加載器加載類的時候就可以先解密,再加載)
用戶自定義加載器實現(xiàn)步驟
- 開發(fā)人員可以通過繼承抽象類 java.lang.ClassLoader 類的方式,實現(xiàn)自己的類加載器,以滿足一些特殊的需求
- 在 JDK1.2 之前,在自定義類加載器時,總會去繼承 ClassLoader 類并重寫 loadClass() 方法,從而實現(xiàn)自定義的類加載類,但是 JDK1.2 之后已經(jīng)不建議用戶去覆蓋 loadClass() 方式,而是建議把自定義的類加載邏輯寫在 findClass() 方法中
- 編寫自定義類加載器時,如果沒有太過于復(fù)雜的需求,可以直接繼承 URLClassLoader 類,這樣就可以避免自己去編寫 findClass() 方法及其獲取字節(jié)碼流的方式,使自定義類加載器編寫更加簡潔
ClassLoader 常用方法
ClassLoader 類,是一個抽象類,其后所有的類加載器都繼承自 ClassLoader(不包括啟動類加載器)
方法 | 描述 |
---|---|
getParent() | 返回該類加載器的超類加載器 |
loadClass(String name) | 加載名稱為name的類,返回java.lang.Class類的實例 |
findClass(String name) | |
findLoadedClass(String name) | 查找名稱為name的已經(jīng)被加載過的類,返回java.lang.Class類的實例 |
defineClass(String name, byte[] b, int off, int len) | 把字節(jié)數(shù)組b中內(nèi)容轉(zhuǎn)換為一個Java類,返回java.lang.Class類的實例 |
resolveClass(Class> c) | 連接指定的一個Java類 |
對類加載器的引用
JVM 必須知道一個類型是由啟動加載器加載的還是由用戶類加載器加載的。如果一個類型是由用戶類加載器加載的,那么 JVM 會將這個類加載器的一個引用作為類型信息的一部分保存在方法區(qū)中。當(dāng)解析一個類型到另一個類型的引用的時候,JVM 需要保證這兩個類型的類加載器是相同的。
雙親委派機(jī)制
Java 虛擬機(jī)對 class 文件采用的是按需加載的方式,也就是說當(dāng)需要使用該類的時候才會將它的 class 文件加載到內(nèi)存生成 class 對象。而且加載某個類的 class 文件時,Java 虛擬機(jī)采用的是雙親委派模式,即把請求交給父類處理,它是一種任務(wù)委派模式。
工作過程
- 如果一個類加載器收到了類加載請求,它并不會自己先去加載,而是把這個請求委托給父類的加載器去執(zhí)行;
- 如果父類加載器還存在其父類加載器,則進(jìn)一步向上委托,依次遞歸,請求最終將到達(dá)頂層的啟動類加載器;
- 如果父類加載器可以完成類加載任務(wù),就成功返回,倘若父類加載器無法完成此加載任務(wù),子加載器才會嘗試自己去加載,這就是雙親委派模式
優(yōu)勢
- 避免類的重復(fù)加載,JVM 中區(qū)分不同類,不僅僅是根據(jù)類名,相同的 class 文件被不同的 ClassLoader 加載就屬于兩個不同的類(比如,Java中的Object類,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的啟動類加載器進(jìn)行加載,如果不采用雙親委派模型,由各個類加載器自己去加載的話,系統(tǒng)中會存在多種不同的 Object 類)
- 保護(hù)程序安全,防止核心 API 被隨意篡改,避免用戶自己編寫的類動態(tài)替換 Java 的一些核心類,比如我們自定義類:java.lang.String
在 JVM 中表示兩個 class 對象是否為同一個類存在兩個必要條件:
- 類的完成類名必須一致,包括包名
- 加載這個類的 ClassLoader(指ClassLoader實例對象)必須相同
沙箱安全機(jī)制
如果我們自定義 String 類,但是在加載自定義 String 類的時候會率先使用引導(dǎo)類加載器加載,而引導(dǎo)類加載器在加載的過程中會先加載 jdk 自帶的文件(rt.jar包中 java\lang\String.class
),報錯信息說沒有 main 方法就是因為加載的是rt.jar
包中的String類。這樣就可以保證對 java 核心源代碼的保護(hù),這就是簡單的沙箱安全機(jī)制。
破壞雙親委派模型
- 雙親委派模型并不是一個強(qiáng)制性的約束模型,而是 Java 設(shè)計者推薦給開發(fā)者的類加載器實現(xiàn)方式,可以“被破壞”,只要我們自定義類加載器,重寫 loadClass() 方法,指定新的加載邏輯就破壞了,重寫 findClass() 方法不會破壞雙親委派。
- 雙親委派模型有一個問題:頂層 ClassLoader,無法加載底層 ClassLoader 的類。典型例子JNDI、JDBC,所以加入了線程上下文類加載器(Thread Context ClassLoader),可以通過Thread.setContextClassLoaser()設(shè)置該類加載器,然后頂層 ClassLoader 再使用 Thread.getContextClassLoader() 獲得底層的 ClassLoader 進(jìn)行加載。
- Tomcat 中使用了自定 ClassLoader,并且也破壞了雙親委托機(jī)制。每個應(yīng)用使用 WebAppClassloader 進(jìn)行單獨加載,他首先使用 WebAppClassloader 進(jìn)行類加載,如果加載不了再委托父加載器去加載,這樣可以保證每個應(yīng)用中的類不沖突。每個tomcat中可以部署多個項目,每個項目中存在很多相同的class文件(很多相同的jar包),他們加載到 jvm 中可以做到互不干擾。
- 利用破壞雙親委派來實現(xiàn)代碼熱替換(每次修改類文件,不需要重啟服務(wù))。因為一個 Class 只能被一個ClassLoader 加載一次,否則會報 java.lang.LinkageError。當(dāng)我們想要實現(xiàn)代碼熱部署時,可以每次都new 一個自定義的 ClassLoader 來加載新的 Class文件。JSP 的實現(xiàn)動態(tài)修改就是使用此特性實現(xiàn)。
關(guān)鍵詞: