聊一聊JVM類(lèi)加載子系統(tǒng)解毒
直擊面試
- 看你簡(jiǎn)歷寫(xiě)得熟悉JVM,那你說(shuō)說(shuō)類(lèi)的加載過(guò)程吧?
- 我們可以自定義一個(gè)String類(lèi)來(lái)使用嗎?
- 什么是類(lèi)加載器,類(lèi)加載器有哪些?
- 多線(xiàn)程的情況下,類(lèi)的加載為什么不會(huì)出現(xiàn)重復(fù)加載的情況?
- 什么是雙親委派機(jī)制?它有啥優(yōu)勢(shì)?可以打破這種機(jī)制嗎?
類(lèi)加載子系統(tǒng)

類(lèi)加載機(jī)制概念
Java虛擬機(jī)把描述類(lèi)的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類(lèi)型,這就是虛擬機(jī)的加載機(jī)制。Class文件由類(lèi)裝載器裝載后,在JVM中將形成一份描述Class結(jié)構(gòu)的元信息對(duì)象,通過(guò)該元信息對(duì)象可以獲知Class的結(jié)構(gòu)信息:如構(gòu)造函數(shù),屬性和方法等,Java允許用戶(hù)借由這個(gè)Class相關(guān)的元信息對(duì)象間接調(diào)用Class對(duì)象的功能,這里就是我們經(jīng)常能見(jiàn)到的Class類(lèi)。
類(lèi)加載子系統(tǒng)作用
- 類(lèi)加載子系統(tǒng)負(fù)責(zé)從文件系統(tǒng)或者網(wǎng)絡(luò)中加載class文件,class文件在文件開(kāi)頭有特定的文件標(biāo)識(shí)(0xCAFEBABE)
- ClassLoader只負(fù)責(zé)class文件的加載。至于它是否可以運(yùn)行,則由Execution Engine決定
- 加載的類(lèi)信息存放于一塊稱(chēng)為方法區(qū)的內(nèi)存空間。除了類(lèi)的信息外,方法區(qū)中還存放運(yùn)行時(shí)常量池信息,可能還包括字符串字面量和數(shù)字常量(這部分常量信息是class文件中常量池部分的內(nèi)存映射)
- Class對(duì)象是存放在堆區(qū)的
類(lèi)加載器ClassLoader角色
class file存在于本地硬盤(pán)上,可以理解為設(shè)計(jì)師畫(huà)在紙上的模板,而最終這個(gè)模板在執(zhí)行的時(shí)候是要加載到JVM當(dāng)中來(lái)根據(jù)這個(gè)文件實(shí)例化出n個(gè)一模一樣的實(shí)例
class file加載到JVM中,被稱(chēng)為DNA元數(shù)據(jù)模板,放在方法區(qū)
在.calss文件 -> JVM -> 最終成為元數(shù)據(jù)模板,此過(guò)程就要一個(gè)運(yùn)輸工具(類(lèi)裝載器),扮演一個(gè)快遞員的角色
類(lèi)加載過(guò)程
類(lèi)從被加載到虛擬機(jī)內(nèi)存中開(kāi)始,到卸載出內(nèi)存為止,它的整個(gè)生命周期包括:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用和卸載七個(gè)階段。(驗(yàn)證、準(zhǔn)備和解析又統(tǒng)稱(chēng)為連接,為了支持Java語(yǔ)言的運(yùn)行時(shí)綁定,所以解析階段也可以是在初始化之后進(jìn)行的。以上順序都只是說(shuō)開(kāi)始的順序,實(shí)際過(guò)程中是交叉的混合式進(jìn)行的,加載過(guò)程中可能就已經(jīng)開(kāi)始驗(yàn)證了)

1. 加載(Loading):
- 通過(guò)一個(gè)類(lèi)的全限定名獲取定義此類(lèi)的二進(jìn)制字節(jié)流
- 將這個(gè)字節(jié)流所代表的的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
- 在內(nèi)存中生成一個(gè)代表這個(gè)類(lèi)的 java.lang.Class 對(duì)象,作為方法區(qū)這個(gè)類(lèi)的各種數(shù)據(jù)的訪(fǎng)問(wèn)入口
加載 .calss 文件的方式
- 從本地系統(tǒng)中直接加載
- 通過(guò)網(wǎng)絡(luò)獲取,典型場(chǎng)景:Web Applet
- 從zip壓縮文件中讀取,成為日后jar、war格式的基礎(chǔ)
- 運(yùn)行時(shí)計(jì)算生成,使用最多的是:動(dòng)態(tài)代理技術(shù)
- 由其他文件生成,比如 JSP 應(yīng)用
- 從專(zhuān)有數(shù)據(jù)庫(kù)提取.class 文件,比較少見(jiàn)
- 從加密文件中獲取,典型的防 Class 文件被反編譯的保護(hù)措施
2. 連接(Linking)
驗(yàn)證(Verify)
- 目的在于確保Class文件的字節(jié)流中包含信息符合當(dāng)前虛擬機(jī)要求,保證被加載類(lèi)的正確性,不會(huì)危害虛擬機(jī)自身安全
- 主要包括四種驗(yàn)證,文件格式驗(yàn)證,元數(shù)據(jù)驗(yàn)證,字節(jié)碼驗(yàn)證,符號(hào)引用驗(yàn)證
準(zhǔn)備(Prepare)
- 為類(lèi)變量分配內(nèi)存并且設(shè)置該類(lèi)變量的默認(rèn)初始值,即零值數(shù)據(jù)類(lèi)型零值int0long0Lshort(short)0char'\\u0000'byte(byte)0booleanfalsefloat0.0fdouble0.0dreferencenull
- 這里不包含用final修飾的static,因?yàn)閒inal在編譯的時(shí)候就會(huì)分配了,準(zhǔn)備階段會(huì)顯示初始化
- 這里不會(huì)為實(shí)例變量分配初始化,類(lèi)變量會(huì)分配在方法區(qū)中,而實(shí)例變量是會(huì)隨著對(duì)象一起分配到Java堆中private static int i = 1; //變量i在準(zhǔn)備階只會(huì)被賦值為0,初始化時(shí)才會(huì)被賦值為1 private final static int j = 2; //這里被final修飾的變量j,直接成為常量,編譯時(shí)就會(huì)被分配為2
解析(Resolve)
- 將常量池內(nèi)的符號(hào)引用轉(zhuǎn)換為直接引用的過(guò)程
- 事實(shí)上,解析操作往往會(huì)伴隨著JVM在執(zhí)行完初始化之后再執(zhí)行
- 符號(hào)引用就是一組符號(hào)來(lái)描述所引用的目標(biāo)。符號(hào)引用的字面量形式明確定義在《Java虛擬機(jī)規(guī)范》的Class文件格式中。直接引用就是直接指向目標(biāo)的指針、相對(duì)偏移量或一個(gè)間接定位到目標(biāo)的句柄
- 解析動(dòng)作主要針對(duì)類(lèi)或接口、字段、類(lèi)方法、接口方法、方法類(lèi)型等。對(duì)應(yīng)常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等
3. 初始化(Initialization)
- 初始化階段就是執(zhí)行類(lèi)構(gòu)造器方法
()的過(guò)程 - 此方法不需要定義,是javac編譯器自動(dòng)收集類(lèi)中的所有類(lèi)變量的賦值動(dòng)作和靜態(tài)代碼塊中的語(yǔ)句合并而來(lái)
- 構(gòu)造器方法中指令按語(yǔ)句在源文件中出現(xiàn)的順序執(zhí)行
()不同于類(lèi)的構(gòu)造器(構(gòu)造器是虛擬機(jī)視角下的 ()) - 若該類(lèi)具有父類(lèi),JVM會(huì)保證子類(lèi)的
()執(zhí)行前,父類(lèi)的 ()已經(jīng)執(zhí)行完畢 - 虛擬機(jī)必須保證一個(gè)類(lèi)的
()方法在多線(xiàn)程下被同步加鎖
- public class ClassInitTest{
- private static int num1 = 30;
- static{
- num1 = 10;
- num2 = 10; //num2寫(xiě)在定義變量之前,為什么不會(huì)報(bào)錯(cuò)呢??
- System.out.println(num2); //這裡直接打印可以嗎?報(bào)錯(cuò),非法的前向引用,可以賦值,但不可調(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
- }
- }
類(lèi)的主動(dòng)使用和被動(dòng)使用
Java程序?qū)︻?lèi)的使用方式分為:主動(dòng)使用和被動(dòng)使用。虛擬機(jī)規(guī)范規(guī)定有且只有5種情況必須立即對(duì)類(lèi)進(jìn)行“初始化”,即類(lèi)的主動(dòng)使用。
- 創(chuàng)建類(lèi)的實(shí)例、訪(fǎng)問(wèn)某個(gè)類(lèi)或接口的靜態(tài)變量,或者對(duì)該靜態(tài)變量賦值、調(diào)用類(lèi)的靜態(tài)方法(即遇到new、getstatic、putstatic、invokestatic這四條字節(jié)碼指令時(shí))
- 反射
- 初始化一個(gè)類(lèi)的子類(lèi)
- Java虛擬機(jī)啟動(dòng)時(shí)被標(biāo)明為啟動(dòng)類(lèi)的類(lèi)
- JDK7 開(kāi)始提供的動(dòng)態(tài)語(yǔ)言支持:java.lang.invoke.MethodHandle實(shí)例的解析結(jié)果,REF_getStatic、REF_putStatic、REF_invokeStatic句柄對(duì)應(yīng)的類(lèi)沒(méi)有初始化,則初始化
除以上五種情況,其他使用Java類(lèi)的方式被看作是對(duì)類(lèi)的被動(dòng)使用,都不會(huì)導(dǎo)致類(lèi)的初始化。
eg:
- public class NotInitialization {
- public static void main(String[] args) {
- //只輸出SupperClass int 123,不會(huì)輸出SubClass init
- //對(duì)于靜態(tài)字段,只有直接定義這個(gè)字段的類(lèi)才會(huì)被初始化
- 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");
- }
- }
類(lèi)加載器
- JVM支持兩種類(lèi)型的類(lèi)加載器,分別為引導(dǎo)類(lèi)加載器(Bootstrap ClassLoader)和自定義類(lèi)加載器(User-Defined ClassLoader)
- 從概念上來(lái)講,自定義類(lèi)加載器一般指的是程序中由開(kāi)發(fā)人員自定義的一類(lèi)類(lèi)加載器,但是Java虛擬機(jī)規(guī)范卻沒(méi)有這么定義,而是將所有派生于抽象類(lèi)ClassLoader的類(lèi)加載器都劃分為自定義類(lèi)加載器
啟動(dòng)類(lèi)加載器(引導(dǎo)類(lèi)加載器,Bootstrap ClassLoader)
- 這個(gè)類(lèi)加載使用C/C++ 語(yǔ)言實(shí)現(xiàn),嵌套在JVM 內(nèi)部
- 它用來(lái)加載Java的核心庫(kù)(JAVA_HOME/jre/lib/rt.jar、resource.jar或sun.boot.class.path路徑下的內(nèi)容),用于提供JVM自身需要的類(lèi)
- 并不繼承自 java.lang.ClassLoader,沒(méi)有父加載器
- 加載擴(kuò)展類(lèi)和應(yīng)用程序類(lèi)加載器,并指定為他們的父類(lèi)加載器
- 出于安全考慮,Boostrap 啟動(dòng)類(lèi)加載器只加載名為java、Javax、sun等開(kāi)頭的類(lèi)
擴(kuò)展類(lèi)加載器(Extension ClassLoader)
- java語(yǔ)言編寫(xiě),由sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn)
- 派生于 ClassLoader
- 父類(lèi)加載器為啟動(dòng)類(lèi)加載器
- 從java.ext.dirs系統(tǒng)屬性所指定的目錄中加載類(lèi)庫(kù),或從JDK的安裝目錄的jre/lib/ext 子目錄(擴(kuò)展目錄)下加載類(lèi)庫(kù)。如果用戶(hù)創(chuàng)建的JAR 放在此目錄下,也會(huì)自動(dòng)由擴(kuò)展類(lèi)加載器加載
應(yīng)用程序類(lèi)加載器(也叫系統(tǒng)類(lèi)加載器,AppClassLoader)
- java語(yǔ)言編寫(xiě),由 sun.misc.Lanucher$AppClassLoader 實(shí)現(xiàn)
- 派生于 ClassLoader
- 父類(lèi)加載器為擴(kuò)展類(lèi)加載器
- 它負(fù)責(zé)加載環(huán)境變量classpath或系統(tǒng)屬性java.class.path 指定路徑下的類(lèi)庫(kù)
- 該類(lèi)加載是程序中默認(rèn)的類(lèi)加載器,一般來(lái)說(shuō),Java應(yīng)用的類(lèi)都是由它來(lái)完成加載的
- 通過(guò) ClassLoader#getSystemClassLoader() 方法可以獲取到該類(lèi)加載器
- public class ClassLoaderTest {
- public static void main(String[] args) {
- //獲取系統(tǒng)類(lèi)加載器
- ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
- System.out.println(systemClassLoader); //sun.misc.Launcher$AppClassLoader@135fbaa4
- //獲取其上層:擴(kuò)展類(lèi)加載器
- ClassLoader extClassLoader = systemClassLoader.getParent();
- System.out.println(extClassLoader); //sun.misc.Launcher$ExtClassLoader@2503dbd3
- //再獲取其上層:獲取不到引導(dǎo)類(lèi)加載器
- ClassLoader bootstrapClassLoader = extClassLoader.getParent();
- System.out.println(bootstrapClassLoader); //null
- //對(duì)于用戶(hù)自定義類(lèi)來(lái)說(shuō),默認(rèn)使用系統(tǒng)類(lèi)加載器進(jìn)行加載,輸出和systemClassLoader一樣
- ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
- System.out.println(classLoader); //sun.misc.Launcher$AppClassLoader@135fbaa4
- //String 類(lèi)使用引導(dǎo)類(lèi)加載器進(jìn)行加載。Java的核心類(lèi)庫(kù)都使用引導(dǎo)類(lèi)加載器進(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());
- }
- }
- }
用戶(hù)自定義類(lèi)加載器
在Java的日常應(yīng)用程序開(kāi)發(fā)中,類(lèi)的加載幾乎是由3種類(lèi)加載器相互配合執(zhí)行的,在必要時(shí),我們還可以自定義類(lèi)加載器,來(lái)定制類(lèi)的加載方式
為什么要自定義類(lèi)加載器?
- 隔離加載類(lèi)
- 修改類(lèi)加載的方式
- 擴(kuò)展加載源(可以從數(shù)據(jù)庫(kù)、云端等指定來(lái)源加載類(lèi))
- 防止源碼泄露(Java代碼容易被反編譯,如果加密后,自定義加載器加載類(lèi)的時(shí)候就可以先解密,再加載)
用戶(hù)自定義加載器實(shí)現(xiàn)步驟
- 開(kāi)發(fā)人員可以通過(guò)繼承抽象類(lèi) java.lang.ClassLoader 類(lèi)的方式,實(shí)現(xiàn)自己的類(lèi)加載器,以滿(mǎn)足一些特殊的需求
- 在JDK1.2之前,在自定義類(lèi)加載器時(shí),總會(huì)去繼承ClassLoader類(lèi)并重寫(xiě)loadClass()方法,從而實(shí)現(xiàn)自定義的類(lèi)加載類(lèi),但是JDK1.2之后已經(jīng)不建議用戶(hù)去覆蓋loadClass()方式,而是建議把自定義的類(lèi)加載邏輯寫(xiě)在findClass()方法中
- 編寫(xiě)自定義類(lèi)加載器時(shí),如果沒(méi)有太過(guò)于復(fù)雜的需求,可以直接繼承URLClassLoader類(lèi),這樣就可以避免自己去編寫(xiě)findClass()方法及其獲取字節(jié)碼流的方式,使自定義類(lèi)加載器編寫(xiě)更加簡(jiǎn)潔
ClassLoader常用方法
ClassLoader類(lèi),是一個(gè)抽象類(lèi),其后所有的類(lèi)加載器都繼承自ClassLoader(不包括啟動(dòng)類(lèi)加載器)
方法描述getParent()返回該類(lèi)加載器的超類(lèi)加載器loadClass(String name)加載名稱(chēng)為name的類(lèi),返回java.lang.Class類(lèi)的實(shí)例findClass(String name)查找名稱(chēng)為name的類(lèi),返回java.lang.Class類(lèi)的實(shí)例findLoadedClass(String name)查找名稱(chēng)為name的已經(jīng)被加載過(guò)的類(lèi),返回java.lang.Class類(lèi)的實(shí)例defineClass(String name, byte[] b, int off, int len)把字節(jié)數(shù)組b中內(nèi)容轉(zhuǎn)換為一個(gè)Java類(lèi),返回java.lang.Class類(lèi)的實(shí)例resolveClass(Class c)連接指定的一個(gè)Java類(lèi)
對(duì)類(lèi)加載器的引用
JVM必須知道一個(gè)類(lèi)型是由啟動(dòng)加載器加載的還是由用戶(hù)類(lèi)加載器加載的。如果一個(gè)類(lèi)型是由用戶(hù)類(lèi)加載器加載的,那么JVM會(huì)將這個(gè)類(lèi)加載器的一個(gè)引用作為類(lèi)型信息的一部分保存在方法區(qū)中。當(dāng)解析一個(gè)類(lèi)型到另一個(gè)類(lèi)型的引用的時(shí)候,JVM需要保證這兩個(gè)類(lèi)型的類(lèi)加載器是相同的。
雙親委派機(jī)制
Java虛擬機(jī)對(duì)class文件采用的是按需加載的方式,也就是說(shuō)當(dāng)需要使用該類(lèi)的時(shí)候才會(huì)將它的class文件加載到內(nèi)存生成class對(duì)象。而且加載某個(gè)類(lèi)的class文件時(shí),Java虛擬機(jī)采用的是雙親委派模式,即把請(qǐng)求交給父類(lèi)處理,它是一種任務(wù)委派模式。
工作過(guò)程
- 如果一個(gè)類(lèi)加載器收到了類(lèi)加載請(qǐng)求,它并不會(huì)自己先去加載,而是把這個(gè)請(qǐng)求委托給父類(lèi)的加載器去執(zhí)行;
- 如果父類(lèi)加載器還存在其父類(lèi)加載器,則進(jìn)一步向上委托,依次遞歸,請(qǐng)求最終將到達(dá)頂層的啟動(dòng)類(lèi)加載器;
- 如果父類(lèi)加載器可以完成類(lèi)加載任務(wù),就成功返回,倘若父類(lèi)加載器無(wú)法完成此加載任務(wù),子加載器才會(huì)嘗試自己去加載,這就是雙親委派模式

優(yōu)勢(shì)
- 避免類(lèi)的重復(fù)加載,JVM中區(qū)分不同類(lèi),不僅僅是根據(jù)類(lèi)名,相同的class文件被不同的ClassLoader加載就屬于兩個(gè)不同的類(lèi)(比如,Java中的Object類(lèi),無(wú)論哪一個(gè)類(lèi)加載器要加載這個(gè)類(lèi),最終都是委派給處于模型最頂端的啟動(dòng)類(lèi)加載器進(jìn)行加載,如果不采用雙親委派模型,由各個(gè)類(lèi)加載器自己去加載的話(huà),系統(tǒng)中會(huì)存在多種不同的Object類(lèi))
- 保護(hù)程序安全,防止核心API被隨意篡改,避免用戶(hù)自己編寫(xiě)的類(lèi)動(dòng)態(tài)替換 Java的一些核心類(lèi),比如我們自定義類(lèi):java.lang.String
在JVM中表示兩個(gè)class對(duì)象是否為同一個(gè)類(lèi)存在兩個(gè)必要條件:
- 類(lèi)的完成類(lèi)名必須一致,包括包名
- 加載這個(gè)類(lèi)的ClassLoader(指ClassLoader實(shí)例對(duì)象)必須相同
沙箱安全機(jī)制
如果我們自定義String類(lèi),但是在加載自定義String類(lèi)的時(shí)候會(huì)率先使用引導(dǎo)類(lèi)加載器加載,而引導(dǎo)類(lèi)加載器在加載的過(guò)程中會(huì)先加載jdk自帶的文件(rt.jar包中java\lang\String.class),報(bào)錯(cuò)信息說(shuō)沒(méi)有main方法就是因?yàn)榧虞d的是rt.jar包中的String類(lèi)。這樣就可以保證對(duì)java核心源代碼的保護(hù),這就是簡(jiǎn)單的沙箱安全機(jī)制。
破壞雙親委派模型
- 雙親委派模型并不是一個(gè)強(qiáng)制性的約束模型,而是Java設(shè)計(jì)者推薦給開(kāi)發(fā)者的類(lèi)加載器實(shí)現(xiàn)方式,可以“被破壞”,只要我們自定義類(lèi)加載器,重寫(xiě)loadClass()方法,指定新的加載邏輯就破壞了,重寫(xiě)findClass()方法不會(huì)破壞雙親委派。
- 雙親委派模型有一個(gè)問(wèn)題:頂層ClassLoader,無(wú)法加載底層ClassLoader的類(lèi)。典型例子JNDI、JDBC,所以加入了線(xiàn)程上下文類(lèi)加載器(Thread Context ClassLoader),可以通過(guò)Thread.setContextClassLoaser()設(shè)置該類(lèi)加載器,然后頂層ClassLoader再使用Thread.getContextClassLoader()獲得底層的ClassLoader進(jìn)行加載。
- Tomcat中使用了自定ClassLoader,并且也破壞了雙親委托機(jī)制。每個(gè)應(yīng)用使用WebAppClassloader進(jìn)行單獨(dú)加載,他首先使用WebAppClassloader進(jìn)行類(lèi)加載,如果加載不了再委托父加載器去加載,這樣可以保證每個(gè)應(yīng)用中的類(lèi)不沖突。每個(gè)tomcat中可以部署多個(gè)項(xiàng)目,每個(gè)項(xiàng)目中存在很多相同的class文件(很多相同的jar包),他們加載到j(luò)vm中可以做到互不干擾。
- 利用破壞雙親委派來(lái)實(shí)現(xiàn)代碼熱替換(每次修改類(lèi)文件,不需要重啟服務(wù))。因?yàn)橐粋€(gè)Class只能被一個(gè)ClassLoader加載一次,否則會(huì)報(bào)java.lang.LinkageError。當(dāng)我們想要實(shí)現(xiàn)代碼熱部署時(shí),可以每次都new一個(gè)自定義的ClassLoader來(lái)加載新的Class文件。JSP的實(shí)現(xiàn)動(dòng)態(tài)修改就是使用此特性實(shí)現(xiàn)。