一篇帶給你JVM 類(lèi)加載過(guò)程解析
類(lèi)加載過(guò)程
類(lèi)加載的時(shí)機(jī)
一個(gè)類(lèi)型被加載到虛擬機(jī)內(nèi)存中開(kāi)始,到卸載出內(nèi)存為止、它的整個(gè)生命周期將會(huì)經(jīng)歷加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用、卸載七個(gè)階段。其中驗(yàn)證、準(zhǔn)備、解析為連接
類(lèi)被主動(dòng)加載的 7 種情況
- 創(chuàng)建類(lèi)的實(shí)例, 比如:new Object();
- 訪問(wèn)某個(gè)類(lèi)或接口的靜態(tài)變量,或者對(duì)該靜態(tài)變量賦值;
- 調(diào)用類(lèi)的靜態(tài)方法;
- 反射(如 Class.forName("com.test.Test");
- 初始化一個(gè)類(lèi)的子類(lèi);
- Java虛擬機(jī)啟動(dòng)時(shí)被標(biāo)記為啟動(dòng)類(lèi)的類(lèi), 就是包含 main 方法的類(lèi)(Java Test);
- JDK1.7開(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)有被初始化則初始化。
其它加載情況
當(dāng) Java 虛擬機(jī)初始化一個(gè)類(lèi)時(shí),要求它所有的父類(lèi)都被初始化,單這一條規(guī)則并不適用于接口。
- 在初始化一個(gè)類(lèi)時(shí),并不會(huì)先初始化它所實(shí)現(xiàn)的接口
- 在初始化一個(gè)接口時(shí),并不會(huì)先初始化它的父類(lèi)接口
- 因此,一個(gè)父接口并不會(huì)因?yàn)樗淖咏涌诨蛘邔?shí)現(xiàn)了類(lèi)的初始化而初始化,只有當(dāng)程序首次被使用特定接口的靜態(tài)變量時(shí),才會(huì)導(dǎo)致該接口的初始化。
只有當(dāng)前程序訪問(wèn)的靜態(tài)變量或靜態(tài)方法確實(shí)在當(dāng)前類(lèi)或當(dāng)前接口定義時(shí),才可認(rèn)為是對(duì)接口或類(lèi)的主動(dòng)使用。
調(diào)用 ClassLoader 類(lèi)的 loadClass 方法加載一類(lèi),并不是對(duì)類(lèi)的主動(dòng)使用,不會(huì)導(dǎo)致類(lèi)的初始化。
測(cè)試?yán)?1:
- public class Test_2 extends Test_2_A {
- static {
- System.out.println("子類(lèi)靜態(tài)代碼塊");
- }
- {
- System.out.println("子類(lèi)代碼塊");
- }
- public Test_2() {
- System.out.println("子類(lèi)構(gòu)造方法");
- }
- public static void main(String[] args) {
- new Test_2();
- }
- }
- class Test_2_A {
- static {
- System.out.println("父類(lèi)靜態(tài)代碼塊");
- }
- {
- System.out.println("父類(lèi)代碼塊");
- }
- public Test_2_A() {
- System.out.println("父類(lèi)構(gòu)造方法");
- }
- public static void find() {
- System.out.println("靜態(tài)方法");
- }
- }
- //代碼塊和構(gòu)造方法執(zhí)行順序
- //1).父類(lèi)靜態(tài)代碼塊
- //2).子類(lèi)靜態(tài)代碼塊
- //3).父類(lèi)代碼塊
- //4).父類(lèi)構(gòu)造方法
- //5).子類(lèi)代碼塊
- //6).子類(lèi)構(gòu)造方法
測(cè)試?yán)?2:
- public class Test_1 {
- public static void main(String[] args) {
- System.out.println(Test_1_B.str);
- }
- }
- class Test_1_A {
- public static String str = "A str";
- static {
- System.out.println("A Static Block");
- }
- }
- class Test_1_B extends Test_1_A {
- static {
- System.out.println("B Static Block");
- }
- }
- //輸出結(jié)果
- //A Static Block
- //A str
類(lèi)加載流程
加載
在硬盤(pán)上查找并且通過(guò) IO 讀入字節(jié)碼文件,使用到該類(lèi)的時(shí)候才會(huì)被加載,例如調(diào)用 main 方法, new 關(guān)鍵字調(diào)用對(duì)象等,在加載階段會(huì)在內(nèi)存中生成這個(gè)類(lèi)的 java.lang.Class 對(duì)象, 作為方法區(qū)這個(gè)類(lèi)的各種數(shù)據(jù)的訪問(wèn)入口。
驗(yàn)證
校驗(yàn)字節(jié)碼文件的正確性
準(zhǔn)備
給類(lèi)的靜態(tài)變量分配內(nèi)存,并且賦予默認(rèn)值
解析
將符號(hào)引用替換為直接引用,該節(jié)點(diǎn)會(huì)把一些靜態(tài)方法(符號(hào)引用,比如 main() 方法)替換為指向數(shù)據(jù)所存內(nèi)存的指針或句柄等(直接引用),這就是所謂的靜態(tài)鏈接過(guò)程(類(lèi)加載期間完成),動(dòng)態(tài)鏈接是在程序運(yùn)行期間完成的將符號(hào)引用替換為直接引用。
初始化
對(duì)類(lèi)的靜態(tài)變量初始化為指定的值,執(zhí)行靜態(tài)代碼塊。
類(lèi)加載器
- **_引導(dǎo)類(lèi)加載器(Bootstrap Class Loader) _**負(fù)責(zé)加載
\lib\ 目錄或者被 -Dbootclaspath 參數(shù)指定的類(lèi), 比如: rt.jar, tool.jar 等 。 - 拓展類(lèi)加載器(Extension Class Loader) 負(fù)責(zé)加載
\lib\ext\ 或 -Djava.ext.dirs 選項(xiàng)所指定目錄下的類(lèi)和 jar包。 - 應(yīng)用程序類(lèi)加載器(System Class Loader) 負(fù)責(zé)加載 CLASSPATH 或 -Djava.class.path所指定的目錄下的類(lèi)和 jar 包。
- 自定義類(lèi)加載器:負(fù)責(zé)加載用戶(hù)自定義包路徑下的類(lèi)包,通過(guò) ClassLoader 的子類(lèi)實(shí)現(xiàn) Class 的加載。
測(cè)試文件:
- public class TestJVMClassLoader {
- public static void main(String[] args) {
- System.out.println(String.class.getClassLoader());
- System.out.println(DESKeyFactory.class.getClassLoader());
- System.out.println(TestJVMClassLoader.class.getClassLoader());
- System.out.println();
- ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
- ClassLoader extClassLoader = appClassLoader.getParent();
- ClassLoader bootstrapClassLoader = extClassLoader.getParent();
- System.out.println("bootstrapClassLoader: " + bootstrapClassLoader);
- System.out.println("extClassLoader: " + extClassLoader);
- System.out.println("appClassLoader: " + appClassLoader);
- System.out.println();
- System.out.println("bootstrapLoader 加載以下文件:");
- URL[] urls = Launcher.getBootstrapClassPath().getURLs();
- for (URL url : urls) {
- System.out.println(url);
- }
- System.out.println();
- System.out.println("extClassLoader 加載以下文件:");
- System.out.println(System.getProperty("java.ext.dirs"));
- System.out.println();
- System.out.println("appClassLoader 加載以下文件:");
- System.out.println(System.getProperty("java.class.path"));
- }
- }
雙親委派機(jī)制
什么是雙親委派機(jī)制?
一個(gè)類(lèi)加載器收到了類(lèi)加載的請(qǐng)求, 它首先不會(huì)自己去嘗試自己去加載這個(gè)類(lèi),而是把這個(gè)請(qǐng)求委派給父類(lèi)加載器去完成,每一個(gè)層次的類(lèi)加載器都是如此,因此所有的請(qǐng)求最終都應(yīng)該傳送到最頂層的啟動(dòng)類(lèi)加載器中,只有當(dāng)父加載器反饋?zhàn)约簾o(wú)法完成這個(gè)加載請(qǐng)求(即搜索范圍中沒(méi)有找到所需的類(lèi))時(shí),子加載器才會(huì)嘗試自己完成加載。
類(lèi)加載和雙親委派模型如下圖所示
我們?cè)賮?lái)看看 ClassLoader 類(lèi)的 loadClass 方法
- // loadClass
- protected Class<?> loadClass(String name, boolean resolve)
- throws ClassNotFoundException
- {
- synchronized (getClassLoadingLock(name)) {
- // 首先檢查當(dāng)前類(lèi)是否被加載
- Class<?> c = findLoadedClass(name);
- if (c == null) {
- long t0 = System.nanoTime();
- try {
- if (parent != null) {
- // 如果父類(lèi)類(lèi)加載器不為空,先嘗試父類(lèi)加載來(lái)加載
- c = parent.loadClass(name, false);
- } else {
- // 引導(dǎo)類(lèi)加載器嘗試加載
- c = findBootstrapClassOrNull(name);
- }
- } catch (ClassNotFoundException e) {
- // ClassNotFoundException thrown if class not found
- // from the non-null parent class loader
- }
- if (c == null) {
- // If still not found, then invoke findClass in order
- // to find the class.
- long t1 = System.nanoTime();
- // 嘗試自己加載
- c = findClass(name);
- // this is the defining class loader; record the stats
- sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
- sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
- sun.misc.PerfCounter.getFindClasses().increment();
- }
- }
- if (resolve) {
- resolveClass(c);
- }
- return c;
- }
- }
- // 類(lèi)加載器的包含關(guān)系
- public abstract class ClassLoader {
- private static native void registerNatives();
- static {
- registerNatives();
- }
- // 當(dāng)前 ClassLoader 和 parent ClassLoader 的包含關(guān)系
- private final ClassLoader parent;
- }
總結(jié):
- 不是樹(shù)形結(jié)構(gòu)(只是邏輯樹(shù)形結(jié)構(gòu)),而是包含/包裝關(guān)系。
- 加載順序,應(yīng)用類(lèi)加載器,拓展加載器,系統(tǒng)加載器。
- 如果有一個(gè)類(lèi)加載器能夠成功加載 Test 類(lèi),那么這個(gè)類(lèi)加載器被稱(chēng)為定義類(lèi)加載器,所有可能返回 Class 對(duì)象引用的類(lèi)加載器(包括定義類(lèi)加載器)都被稱(chēng)為初始類(lèi)加載器。
設(shè)計(jì)雙親委派機(jī)制的目的?
- 保證 Java 核心庫(kù)的類(lèi)型安全:所有的java 應(yīng)用都會(huì)至少引用 java.lang.Object 類(lèi), 也就是說(shuō)在運(yùn)行期, java.lang.Object 的這個(gè)類(lèi)會(huì)被加載到 Java 虛擬機(jī)中,如果這個(gè)加載過(guò)程是由 Java 應(yīng)用自己的類(lèi)加載器所完成的,那么很有可能會(huì)在 JVM 中存在多個(gè)版本的 java.lang.Object 類(lèi),而且這些類(lèi)之間還是不兼容的。互不可見(jiàn)的(正是命名空間發(fā)揮著作用)借助于雙親委托機(jī)制,Java 核心庫(kù)中的類(lèi)加載工作都是由啟動(dòng)類(lèi)加載器統(tǒng)一來(lái)完成的。從而確保了Java 應(yīng)用所使用的都是同一個(gè)版本的 Java 核心類(lèi)庫(kù),他們之間是相互兼容的。
- 可以確保 Java 核心庫(kù)所提供的類(lèi)不會(huì)被自定義的類(lèi)所替代。
- 不同的類(lèi)加載器可以為相同類(lèi)(binary name)的類(lèi)創(chuàng)建額外的命名空間。相同名稱(chēng)的類(lèi)可以并存在Java虛擬機(jī)中,只需要不同的類(lèi)加載器來(lái)加載他們即可,不同的類(lèi)加載器的類(lèi)之間是不兼容的,這相當(dāng)于在JAVA虛擬機(jī)內(nèi)部創(chuàng)建了一個(gè)又一個(gè)相互隔離的Java類(lèi)空間,這類(lèi)技術(shù)在很多框架中得到了實(shí)際運(yùn)用。
自定義類(lèi)加載器
自定義類(lèi)加載器加載類(lèi),下面是一個(gè)簡(jiǎn)單的 Demo
- import java.io.ByteArrayOutputStream;
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.InputStream;
- public class ClassLoaderTest extends ClassLoader {
- private static String rxRootPath;
- static {
- rxRootPath = "/temp/class/";
- }
- @Override
- public Class findClass(String name) {
- byte[] b = loadClassData(name);
- return defineClass(name, b, 0, b.length);
- }
- /**
- * 讀取 .class 文件為字節(jié)數(shù)組
- *
- * @param name 全路徑類(lèi)名
- * @return
- */
- private byte[] loadClassData(String name) {
- try {
- String filePath = fullClassName2FilePath(name);
- InputStream is = new FileInputStream(new File(filePath));
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- byte[] buf = new byte[2048];
- int r;
- while ((r = is.read(buf)) != -1) {
- bos.write(buf, 0, r);
- }
- return bos.toByteArray();
- } catch (Throwable e) {
- e.printStackTrace();
- }
- return null;
- }
- /**
- * 全限定名轉(zhuǎn)換為文件路徑
- *
- * @param name
- * @return
- */
- private String fullClassName2FilePath(String name) {
- return rxRootPath + name.replace(".", "//") + ".class";
- }
- public static void main(String[] args) throws ClassNotFoundException {
- ClassLoaderTest classLoader = new ClassLoaderTest();
- String className = "com.test.TestAA";
- Class clazz = classLoader.loadClass(className);
- System.out.println(clazz.getClassLoader());
- // 輸出結(jié)果
- //cn.xxx.xxx.loader.ClassLoaderTest@3764951d
- }
- }
Tomcat 類(lèi)加載器
Tomcat 中的類(lèi)加載器模型
Tomcat 類(lèi)加載器說(shuō)明
tomcat 的幾個(gè)主要類(lèi)加載器:
- commonLoader:Tomcat 最基本的類(lèi)加載器, 加載路徑中的 class 可以被 Tomcat 容器本身以及各個(gè) WebApp 訪問(wèn)。
- catalinaLoader:Tomcat 容器私有的類(lèi)加載器 加載路徑中的 class 對(duì)于 Webapp 不可見(jiàn);
- sharaLoader: 各個(gè)Webapp 共享的類(lèi)加載器, 加載路徑中的 class 對(duì)于所有 webapp 可見(jiàn), 但是對(duì)于 Tomcat 容器不可見(jiàn)。
- webappLoader: 各個(gè) Webapp 私有的類(lèi)加載, 加載路徑中的 class 只對(duì)當(dāng)前 webapp 可見(jiàn), 比如加載 war 包里面相關(guān)的類(lèi),每個(gè) war 包應(yīng)用都有自己的 webappClassLoader 對(duì)象,對(duì)應(yīng)不同的命名空間,實(shí)現(xiàn)相互隔離,比如 war 包中可以引入不同的 spring 版本,實(shí)現(xiàn)多個(gè) spring 版本 應(yīng)用的同時(shí)運(yùn)行。
總結(jié):
從圖中的委派關(guān)系中可以看出:
Commonclassloader 能加載的類(lèi)都可以被 Catalinaclassloader和 Sharedclassloadert 使用, 從而實(shí)現(xiàn)了公有類(lèi)庫(kù)的共用,而Catalinaclassloader 和 Sharedclassloader自己能加載的類(lèi)則與對(duì)方相互隔離 Webappclassloader 可以使用 Shared Loader 加載到的類(lèi),但各個(gè) Webappclassloader 實(shí)例之間相互隔離而 Jasper Loader 的加載范圍僅僅是這個(gè) JSP 文件所編譯出來(lái)的那一個(gè) . class 文件,它出現(xiàn)的目的就是為了被丟棄: 當(dāng) Web 容器檢測(cè)到 JSP 文件被修改時(shí),會(huì)替換掉目前的 Jasperloader 的實(shí)例,并通過(guò)再建立一個(gè)新的 Jsp 類(lèi)加載器來(lái)實(shí)現(xiàn) JSP 文件的熱加載功能。
Tomcat這種類(lèi)加載機(jī)制違背了java推薦的雙親委派模型了嗎? 答案是: 違背了
tomcat不是這樣實(shí)現(xiàn), tomcat為了實(shí)現(xiàn)隔離性, 沒(méi)有遵守這個(gè)約定, 每個(gè) webapp Loader加載自己的目錄下的 class'文件,不會(huì)傳遞給父類(lèi)加載器,打破了雙親委派機(jī)制
參考資料
《深入理解 Java 虛擬機(jī)》 第三版 周志明
Apache Tomcat Documentation