關(guān)于Java類加問題我竟讓面試官啞口無言
學(xué)習(xí)類加載之前我們先看看從面試官的角度會(huì)問哪些問題?畢竟帶著問題學(xué)習(xí)會(huì)比較高效。
直擊面試
看你簡歷寫得熟悉 JVM,那你說說類的加載過程吧?
我們可以自定義一個(gè) String 類來使用嗎?
什么是類加載器,類加載器有哪些?這些類加載器都加載哪些文件?
多線程的情況下,類的加載為什么不會(huì)出現(xiàn)重復(fù)加載的情況?
什么是雙親委派機(jī)制?它有啥優(yōu)勢?可以打破這種機(jī)制嗎?
類加載子系統(tǒng)

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

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

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