詳細(xì)分析JVM類加載機(jī)制
前言
ladies and gentleman , 你們好?? ,我是羨羨 , 這節(jié)我們進(jìn)入jvm的學(xué)習(xí) , 我們知道 , jvm是java虛擬機(jī), java代碼的執(zhí)行與 jvm 息息相關(guān), 接下來我們來依次介紹 , 首先這節(jié)先來介紹 jvm 中的類加載部分
1. jvm 的組成
jvm組成可分為這四個(gè)部分
1.類加載器(ClassLoader)
2.運(yùn)行時(shí)數(shù)據(jù)區(qū)(Runtime Data Area)
3.執(zhí)行引擎(ExecutionEngine)
4.本地庫接口(Native Interface)
那么一個(gè)程序在 jvm 中的運(yùn)行過程是怎樣的呢?
java代碼首先被編譯成字節(jié)碼文件(Class文件), 通過不同操作系統(tǒng)上的 jvm 來加載解釋 , 這個(gè)過程首先需要類加載器加載class文件 , 然后進(jìn)行字節(jié)碼校驗(yàn) , 校驗(yàn)結(jié)束通過后通過jvm解釋器翻譯成機(jī)器碼交給操作系統(tǒng)執(zhí)行
程序在執(zhí)行之前先要把 java 代碼轉(zhuǎn)換成字節(jié)碼(class 文件),jvm 首先需要把字節(jié)碼通一定的方式 類加載器(ClassLoader) 把文件加載到內(nèi)存中的運(yùn)行時(shí)數(shù)據(jù)區(qū)(Runtime DataArea) ,而字節(jié)碼文件是 jvm 的一套指 令集規(guī)范,并不能直接交個(gè)底層操作系統(tǒng)去執(zhí)行,因此需要特定的命令解析器 執(zhí)行引擎(Execution Engine) 將字節(jié)碼翻譯成底層系統(tǒng)指令再交由 CPU 去執(zhí)行,而這個(gè)過程中需要調(diào)用其他語言的接口 本地庫接口(Native Interface) 來實(shí)現(xiàn)整個(gè)程序的功能,這就是這 4 個(gè)主要組成部分的職責(zé)與功能
jvm整體結(jié)構(gòu)如下圖
可能說到這里 , 初學(xué)的同學(xué)會(huì)有點(diǎn)懵 , 不過不用擔(dān)心, 上圖中的各個(gè)組成部分后面都會(huì)一一解釋
2. 類加載
通過上面 jvm的運(yùn)行過程可知 , 類加載就是讀取 class文件的過程 , 這期間需要用到類加載器 , 類加載器只負(fù)責(zé)加載 , 至于如何執(zhí)行 , 則由執(zhí)行引擎決定
類加載 又分為以下幾個(gè)模塊
類加載器加載class文件到 jvm中 , 被稱為DNA元數(shù)據(jù)模板 , jvm通過模板來創(chuàng)建實(shí)例 , 類加載器在此過程相當(dāng)于擔(dān)任了快遞員的角色
那么類加載的過程是怎樣的呢 ?
可以看到 , 分為三個(gè)過程 : 加載, 鏈接 ,初始化
1. 加載
1. 通過類名(地址)獲取此類的二進(jìn)制字節(jié)流.
2. 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)(元空間)的運(yùn)行時(shí)結(jié)構(gòu).
3. 在內(nèi)存中生成一個(gè)代表這個(gè)類的 java.lang.Class 對(duì)象,作為這個(gè)類的各種數(shù)據(jù)的訪問入口.
顧名思義 , 加載就是把類中的信息加載進(jìn) jvm 中
2. 鏈接
鏈接又分為 3 個(gè)過程 : 驗(yàn)證, 準(zhǔn)備 ,解析
(1) . 驗(yàn)證
檢驗(yàn)被加載的類是否有正確的內(nèi)部結(jié)構(gòu),并和其他類協(xié)調(diào)一致
驗(yàn)證文件格式是否一致: class 文件在文件開頭有特定的文件標(biāo)識(shí)(字節(jié)碼文件都以 CA FE BA BE 標(biāo)識(shí)開頭);主,次版本號(hào)是否在當(dāng)前 java 虛擬機(jī)接收范圍內(nèi)
元數(shù)據(jù)驗(yàn)證:對(duì)字節(jié)碼描述的信息進(jìn)行語義分析,以保證其描述的信息符合java 語言規(guī)范的要求,例如這個(gè)類是否有父類;是否繼承瀏覽不允許被繼承的類(final 修飾的類)
驗(yàn)證過程主要是看是否符合java語言的規(guī)范
(2) . 準(zhǔn)備
準(zhǔn)備:準(zhǔn)備階段則負(fù)責(zé)為類的靜態(tài)屬性分配內(nèi)存,并設(shè)置默認(rèn)初始值(int為0)
此過程不包含用 final 修飾的 static 常量(靜態(tài)常量),在編譯時(shí)進(jìn)行初始化.
//準(zhǔn)備階段值為0 public static int value = 123; //準(zhǔn)備階段值為123 public static final int value = 123;
(3) .解析
將類的二進(jìn)制數(shù)據(jù)中的符號(hào)引用替換成直接引用(符號(hào)引用是 Class 文件的邏輯符號(hào),直接引用指向的方法區(qū)中某一個(gè)地址)
將符號(hào)引用替換成直接引用, 這句話怎么理解呢 ? 這里來舉個(gè)例子
public void method1(){ method2(); }
在 方法1 中調(diào)用 方法2 , 我們這樣來寫代碼的時(shí)候, 這就只是符號(hào)引用 , 而當(dāng)這段程序被加載進(jìn) jvm解析的時(shí)候 符號(hào)引用就會(huì)變成直接引用, 也就是指明此處真正的引用地址
3. 初始化
在談?lì)惖某跏蓟^程之前, 先來考慮 , 類什么時(shí)候會(huì)被初始化?
1 )創(chuàng)建類的實(shí)例,也就是 new 一個(gè)對(duì)象
2)訪問某個(gè)類或接口的靜態(tài)變量,或者對(duì)該靜態(tài)變量賦值
3)調(diào)用類的靜態(tài)方法
4)反射(Class.forName(“”))
5)初始化一個(gè)類的子類(會(huì)首先初始化子類的父類)
初始化類的過程也是為類中成員賦值的過程 , 在鏈接過程中的準(zhǔn)備過程中被static修飾的變量是賦了默認(rèn)值(int型為0), 而在初始化過程中才會(huì)賦予我們賦的值
我們常說 , 用 static 修飾的變量, 方法 , 代碼塊是跟類直接打交道的 , 我們說加載類的時(shí)候, 使用static修飾的成員也會(huì)被加載 , 此過程也是在類的初始化中完成
那么在初始化過程中, 賦值順序是怎樣的呢?
如果同時(shí)包含多個(gè)靜態(tài)變量和靜態(tài)代碼塊,則按照自上而下的順序依次執(zhí)行。 如果初始化一個(gè)類的時(shí)候,其父類尚未初始化,則優(yōu)先初始化其父類。
順序是:父類 static –> 子類 static –> 父類構(gòu)造方法- -> 子類構(gòu)造方法
下面代碼 num 的值變化
3. 類加載器
從開發(fā)人員的角度上來講, 類加載器可分為3類 : 引導(dǎo)類加載器(啟動(dòng)類加載器), 擴(kuò)展類加載器 , 應(yīng)用程序類加載器
引導(dǎo)類加載器(啟動(dòng)類加載器)
這個(gè)類加載器使用 C/C++語言實(shí)現(xiàn),嵌套在 JVM 內(nèi)部.它用來加載 java 核心類庫.
并不繼承于java.lang.ClassLoader , 沒有父加載器 , 負(fù)責(zé)加載擴(kuò)展類加載器和應(yīng)用程序類加載器 , 并為它們指定父類加載器
ClassLoader 類,它是一個(gè)抽象類,其后所有的類加載器都繼承自 ClassLoader (不包括啟動(dòng)類加載器)
引導(dǎo)類加載器作為頂級(jí)的類加載器, 非java語言實(shí)現(xiàn) , 所以和java中其他類加載器也不存在繼承關(guān)系等
擴(kuò)展類加載器
Java 語言編寫的,由sun.misc.Launcher$ExtClassLoader 實(shí)現(xiàn). 派生于 ClassLoader 類.
負(fù)責(zé)從 java.ext.dirs 系統(tǒng)屬性所指定的目錄中加載類庫,或從 JDK 系統(tǒng)安裝目錄jre/lib/ext 子目錄(擴(kuò)展目錄)下加載類庫.如果用戶創(chuàng)建的 jar 放在此目錄下,也會(huì)自動(dòng)由擴(kuò)展類加載器加載
應(yīng)用程序類加載器
Java 語言編寫的,由 sun.misc.Launcher$AppClassLoader 實(shí)現(xiàn). 派生于 ClassLoader 類. 加載我們自己定義的類,用于加載用戶類路徑(classpath)上所有的類. 該類加載器是程序中默認(rèn)的類加載器.
我們自己寫的類是由應(yīng)用程序類加載器加載的, 類加載器結(jié)構(gòu)示例如下
//獲取應(yīng)用程序類加載器 sun.misc.Launcher$AppClassLoader@18b4aac2 ClassLoader classLoader = ClassLoader.getSystemClassLoader(); System.out.println(classLoader); //獲得父類加載器 sun.misc.Launcher$ExtClassLoader@74a14482 System.out.println(classLoader.getParent()); //擴(kuò)展類加載器上一級(jí)是引導(dǎo)類加載器,不是java實(shí)現(xiàn),為null System.out.println(classLoader.getParent().getParent()); //拿到String類的類加載器,結(jié)果為null //可見,String類為引導(dǎo)類加載器加載 ClassLoader classLoader1 = String.class.getClassLoader(); System.out.println(classLoader1);
另外還有一種叫做用戶自定義類加載器 , 例如 tomcat
4. 雙親委派機(jī)制
什么是雙親委派機(jī)制呢?
Java 虛擬機(jī)對(duì) class 文件采用的是按需加載的方式,也就是說當(dāng)需要該類時(shí)才會(huì) 將它的class 文件加載到內(nèi)存中生成 class 對(duì)象.而且加載某個(gè)類的 class 文件 時(shí),Java 虛擬機(jī)采用的是雙親委派模式,即把請(qǐng)求交由父類處理,它是一種任務(wù)委 派模式.
.
就是說呢, 如果類加載器接收到了加載請(qǐng)求, 并不會(huì)去立即加載這個(gè)類, 而是把請(qǐng)求交給它的上一級(jí)加載器去加載 , 上一級(jí)沒有則繼續(xù)往上找 , 直到頂級(jí)的類加載器(引導(dǎo)類加載器)也無法加載時(shí), 開始往下找 , 如果有一級(jí)加載成功則返回, 最終加載器都無法加載時(shí), 就會(huì)拋出ClassNotFoundException異常
那么為什么要這樣去做呢? 試想, 我們自己創(chuàng)建一個(gè)java.lang.String類
package java.lang; public class String { public String(){ System.out.println("自己的String"); } }
建立一個(gè)測(cè)試類
public class TestString { public static void main(String[] args) { new String(); } }
試想 , "自己的String" 這句話會(huì)被輸出嗎 ? 答案肯定是不會(huì)
因?yàn)榧虞d類的時(shí)候會(huì)先往上走, 此時(shí)走到了引導(dǎo)類加載器, 引導(dǎo)類加載器發(fā)現(xiàn)此類沒有被加載,并且自己可以加載, 那么java.lang.String 就會(huì)被加載了, 此時(shí)就會(huì)直接返回
那么雙親委派機(jī)制出現(xiàn)的原因就顯而易見了
安全,可避免用戶自己編寫的類動(dòng)態(tài)替換 Java 的核心類,如 java.lang.String , 避免全限定命名的類重復(fù)加載(使用了 findLoadClass()判斷當(dāng)前類是否已加載)
5. 類的主動(dòng)/被動(dòng)使用
JVM 規(guī)定,每個(gè)類或者接口被首次主動(dòng)使用時(shí)才對(duì)其進(jìn)行初始化,有主動(dòng)使用,自然就有被動(dòng)使用.那么什么時(shí)候類被主動(dòng)使用呢?
- 通過new關(guān)鍵字被導(dǎo)致類的初始化,這是大家經(jīng)常使用的初始化一個(gè)類的方式,他肯定會(huì)導(dǎo)致類的加載并且初始化
- 訪問類的靜態(tài)變量,包括讀取和更新
- 訪問類的靜態(tài)方法
- 對(duì)某個(gè)類進(jìn)行反射操作,會(huì)導(dǎo)致類的初始化
- 初始化子類會(huì)導(dǎo)致父類的的初始化
- 執(zhí)行該類的 main 函數(shù)
除了上面的幾種主動(dòng)使用其余就是被動(dòng)使用了
1.引用該類的靜態(tài)常量,注意是常量,不會(huì)導(dǎo)致初始化,但是也有意外,這里的常量是指已經(jīng)指定字面量的常量,對(duì)于那些需要一些計(jì)算才能得出結(jié)果的常量就會(huì)導(dǎo)致初始化,比如:
public final static int NUMBER = 5 ; //不會(huì)導(dǎo)致類初始化,被動(dòng)使用 public final static int RANDOM = new Random().nextInt() ; //會(huì)導(dǎo)致類的初始化,主動(dòng)使用
2.構(gòu)造某個(gè)類的數(shù)組時(shí)不會(huì)導(dǎo)致該類的初始化
Student[] students = new Student[10]
主動(dòng)使用和被動(dòng)使用的區(qū)別在于類是否會(huì)被初始化.
結(jié)語
到此關(guān)于 jvm 類加載這一章就說完了 , 感謝您的閱讀 , 后續(xù)將會(huì)進(jìn)行 jvm 中運(yùn)行時(shí)數(shù)據(jù)區(qū)的講解 , 感謝您的支持 ,謝謝 !!!
到此這篇關(guān)于詳細(xì)分析JVM類加載機(jī)制的文章就介紹到這了,更多相關(guān)JVM類加載內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java程序圖形用戶界面設(shè)計(jì)之標(biāo)簽組件
圖形界面(簡(jiǎn)稱GUI)是指采用圖形方式顯示的計(jì)算機(jī)操作用戶界面。與早期計(jì)算機(jī)使用的命令行界面相比,圖形界面對(duì)于用戶來說在視覺上更易于接受,本篇精講Java語言中關(guān)于圖形用戶界面的標(biāo)簽組件部分2022-02-02Java 如何使用@Autowired注解自動(dòng)注入bean
這篇文章主要介紹了Java 使用@Autowired注解自動(dòng)注入bean的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06Spring-Cloud-Function-Spel?漏洞環(huán)境搭建
這篇文章主要介紹了Spring-Cloud-Function-Spel?漏洞復(fù)現(xiàn)及搭建方法,搭建方法也很簡(jiǎn)單,首先需要安裝maven jdk,具體安裝過程跟隨小編一起看看吧2022-03-03feign遠(yuǎn)程調(diào)用無法傳遞對(duì)象屬性405的問題
這篇文章主要介紹了feign遠(yuǎn)程調(diào)用無法傳遞對(duì)象屬性405的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03JAXB命名空間及前綴_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要給大家介紹了關(guān)于JAXB命名空間及前綴的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-08-08Java 獲取服務(wù)器環(huán)境的實(shí)例詳解
這篇文章主要介紹了Java 獲取服務(wù)器環(huán)境的實(shí)例詳解的相關(guān)資料,這里提供實(shí)例和輸出結(jié)果,希望能幫助大家理解,需要的朋友可以參考下2017-07-07