描述
本文翻譯Understanding JVM Architecture部分。文中介紹JVM包含類加載子系統(tǒng)仑嗅、運(yùn)行時(shí)數(shù)據(jù)區(qū)宴倍、執(zhí)行引擎。
JVM架構(gòu)
JVM 只是一種規(guī)范仓技,其實(shí)現(xiàn)因廠商而異鸵贬。 現(xiàn)在,讓我們了解一下普遍接受的 JVM 的體系結(jié)構(gòu)脖捻。
1 Class Loader 子系統(tǒng)
JVM 駐留在 RAM 上阔逼。 在執(zhí)行期間,使用 Class Loader 子系統(tǒng)地沮,將類文件傳送到 RAM嗜浮。 這就是所謂的 Java 的動(dòng)態(tài)類加載功能。 JVM 在運(yùn)行中第一次引用一個(gè)類時(shí)(而不是編譯時(shí))摩疑,加載危融、鏈接和初始化.class
文件。
1.1 加載
Class Loader 的主要任務(wù)是加載.class
文件到內(nèi)存中雷袋。 通常吉殃,類加載過程從加載主類(即具有static main()
方法聲明的類)開始。 所有后續(xù)的類加載嘗試都是根據(jù)已經(jīng)運(yùn)行的類中的類引用完成的,如下面所提到的:
- 當(dāng)字節(jié)碼中對(duì)一個(gè)類進(jìn)行靜態(tài)引用時(shí)(例如:
System.out
) - 當(dāng)字節(jié)碼中創(chuàng)建一個(gè)對(duì)象時(shí)(例如:
Person person = new Person("John")
)
有3種類型的類加載器(繼承關(guān)系) 蛋勺,它們遵循4個(gè)主要原則瓦灶。
1.1.1 能見度原則
這個(gè)原則指出,子類加載器可以看到父類加載器加載的類抱完,但是父類加載器不能找到子類加載器加載的類贼陶。
1.1.2 唯一性原則
這個(gè)原則聲明父類加載的類不應(yīng)該再次被子類加載器加載,并確保不會(huì)發(fā)生重復(fù)的類加載乾蛤。
1.1.3 授權(quán)等級(jí)原則
為了滿足上述兩個(gè)原則每界,JVM 遵循一個(gè)委托層次結(jié)構(gòu),為每個(gè)類加載請(qǐng)求選擇類加載器家卖。 這里眨层,從最低的子級(jí)開始,Application Class Loader
將接收到的類加載請(qǐng)求委托給Extension Class Loader
上荡,然后Extension Class Loader
將請(qǐng)求委托給Bootstrap Class Loader
趴樱。 如果在 Bootstrap 路徑中找到請(qǐng)求的類,則加載該類酪捡。 否則叁征,請(qǐng)求將再次傳回Extension Class Loader
級(jí)別,以從擴(kuò)展路徑或自定義指定的路徑查找類逛薇。 如果它也失敗了捺疼,那么請(qǐng)求返回到Application Class Loader
,從 System 類路徑查找類永罚,如果Application Class Loader
加載請(qǐng)求的類也失敗啤呼,那么我們就會(huì)得到運(yùn)行時(shí)異常java.lang.ClassNotFoundException
。
1.1.4 不卸載原則
即使Class Loader
可以加載類呢袱,但是不能卸載已加載的類官扣。 可以刪除當(dāng)前的Class Loader
而不是卸載,并創(chuàng)建一個(gè)新的Class Loader
羞福。
- BootStrap Class Loader 從rt.jar中加載標(biāo)準(zhǔn)JDK類惕蹄,比如引導(dǎo)路徑
$JAVA_HOME/jre/lib
目錄中核心Java API類(例如java.lang.*
包下的類)。它是用 c/c++ 這樣的本地語言實(shí)現(xiàn)的治专,在 Java 中充當(dāng)所有類加載器的父類卖陵。 - Extension Class Loader 將類加載請(qǐng)求委托給它的父級(jí) Bootstrap,如果不成功张峰,則從擴(kuò)展目錄
$JAVA_HOME/jre/lib/ext
或者由java.ext.dirs
系統(tǒng)屬性指定的任何其他目錄加載類泪蔫。這個(gè)類加載器sun.misc.Launcher$ExtClassLoader
由 Java 實(shí)現(xiàn)。 - Application Class Loader 從系統(tǒng)類路徑加載應(yīng)用程序特定的類挟炬,調(diào)用程序時(shí)使用命令行選項(xiàng)
-cp
或-classpath
設(shè)置這些類鸥滨。 它內(nèi)部使用映射到java.class.path
的環(huán)境變量。 這個(gè)類加載器是sun.misc.Launcher$AppClassLoader
由 Java 實(shí)現(xiàn)的谤祖。
注意:除了上述3個(gè)類加載器之外婿滓,程序員可以在代碼中直接創(chuàng)建自定義類加載器。類加載器委托模型保證了應(yīng)用程序的獨(dú)立性粥喜。這種方法用于網(wǎng)絡(luò)應(yīng)用服務(wù)器凸主,比如 Tomcat,使網(wǎng)絡(luò)應(yīng)用和企業(yè)應(yīng)用能夠獨(dú)立運(yùn)行额湘。
每個(gè)類加載器都有其存儲(chǔ)已加載類的命名空間卿吐。 當(dāng)類加載器加載一個(gè)類時(shí),它會(huì)根據(jù)命名空間中存儲(chǔ)的全限定類名(Fully Qualified Class Name)搜索類锋华,以檢查類是否已經(jīng)加載嗡官。 即使該類具有相同的全限定類名但是有不同的命名空間,它也被視為不同的類毯焕。 不同的命名空間表示該類已由另一個(gè)類加載器加載過衍腥。
1.2 鏈接
鏈接涉及到驗(yàn)證和準(zhǔn)備加載后的類或接口、它的直接超類和超接口纳猫,以及必需的元素類型婆咸,同時(shí)遵循以下屬性。
- 類或接口必須在鏈接之前完成加載
- 類或接口在初始化(下一步)之前必須完成驗(yàn)證和準(zhǔn)備
- 如果在鏈接過程中出現(xiàn)錯(cuò)誤芜辕,它會(huì)被拋出到程序中的某個(gè)點(diǎn)上尚骄,在這個(gè)點(diǎn)上,程序?qū)⒉扇∫恍┬袆?dòng)侵续,這些行動(dòng)可能直接或間接地要求鏈接到錯(cuò)誤中涉及的類或接口
鏈接分為以下三個(gè)階段倔丈。
-
驗(yàn)證:確保
.class
文件正確性。代碼是否按照 Java 語言規(guī)范正確編寫询兴? 代碼是否由一個(gè)有效的編譯器根據(jù) JVM 規(guī)范生成乃沙?這是類加載過程中最復(fù)雜的驗(yàn)證過程,并且花費(fèi)的時(shí)間最長诗舰。盡管鏈接減慢了類加載過程警儒,但在執(zhí)行字節(jié)碼時(shí),它避免了多次執(zhí)行這些檢查眶根,從而使整個(gè)執(zhí)行高效蜀铲。如果驗(yàn)證失敗,它將拋出運(yùn)行時(shí)錯(cuò)誤java.lang.VerifyError
属百。例如记劝,執(zhí)行以下檢查:- 符號(hào)表一致且格式正確
- final修飾的method/class 不能被重寫/繼承
- 方法遵循訪問修飾符
- 方法參數(shù)數(shù)量和類型正確
- 字節(jié)碼不能錯(cuò)誤地操作棧
- 變量讀之前必須初始化
- 變量值的類型正確
準(zhǔn)備:為靜態(tài)變量和 JVM 使用的任何數(shù)據(jù)結(jié)構(gòu)(如方法表)分配內(nèi)存。靜態(tài)字段被創(chuàng)建并初始化默認(rèn)值族扰,但是厌丑,在這個(gè)階段不會(huì)執(zhí)行初始化器定欧,因?yàn)檫@是初始化的一部分
解析:用直接引用替換類型中的符號(hào)引用。 它是通過搜索方法區(qū)來定位被引用的實(shí)體
1.3 初始化
在這里怒竿,將執(zhí)行每個(gè)加載過的類或接口的初始化邏輯(例如調(diào)用類的構(gòu)造函數(shù))砍鸠。由于 JVM 是多線程的,對(duì)類或接口的初始化應(yīng)該非常小心地進(jìn)行耕驰,并進(jìn)行適當(dāng)?shù)耐揭瑁员苊馄渌€程同時(shí)嘗試初始化同一個(gè)類或接口(確保初始化線程安全)。
這是類加載的最后階段朦肘,所有的靜態(tài)變量都被賦予了代碼中定義的原始值饭弓,并且靜態(tài)塊將被執(zhí)行(如果有的話)。 這在類中從上到下逐行執(zhí)行媒抠,在類層次結(jié)構(gòu)中從父級(jí)到子級(jí)執(zhí)行弟断。
2 運(yùn)行時(shí)數(shù)據(jù)區(qū)
運(yùn)行時(shí)數(shù)據(jù)區(qū)域是 JVM 程序在操作系統(tǒng)上運(yùn)行時(shí)分配的內(nèi)存區(qū)域。 除了閱讀.class
文件趴生,類加載器子系統(tǒng)還生成相應(yīng)的二進(jìn)制數(shù)據(jù)夫嗓,并在方法區(qū)中為每個(gè)類分別保存以下信息。
- 加載過的類及其直接父類的完全限定名
-
.class
文件是否與 Class/Interface/Enum 相關(guān) - 修飾符冲秽、靜態(tài)變量和方法信息等
然后舍咖,為每一個(gè)加載過的.class
文件,創(chuàng)建一個(gè) Class 對(duì)象來表示在方法區(qū)中定義的文件锉桑。 這個(gè) Class 對(duì)象可以用來讀取后面代碼中的類級(jí)別信息(類名排霉、父名、方法民轴、變量信息攻柠、靜態(tài)變量等)。
2.1 方法區(qū)(線程共享)
這是一個(gè)共享資源(每個(gè) JVM 只有1個(gè)方法區(qū))后裸。 所有 JVM 線程都共享這個(gè)方法區(qū)瑰钮,因此對(duì)方法數(shù)據(jù)和動(dòng)態(tài)鏈接的訪問必須是線程安全的。
方法區(qū)存儲(chǔ)類級(jí)別數(shù)據(jù)(包括靜態(tài)變量) 微驶,如:
- Class Loader 引用
- 運(yùn)行時(shí)常量池——數(shù)值常量浪谴、字段引用、方法引用因苹、屬性苟耻;以及每個(gè)類和接口的常量,它包含方法和字段的所有引用扶檐。 當(dāng)引用某個(gè)方法或字段時(shí)凶杖,JVM 在運(yùn)行時(shí)常量池中搜索該方法或字段在內(nèi)存中的實(shí)際地址
- 字段數(shù)據(jù)——每個(gè)字段: 名稱、類型款筑、修飾符智蝠、屬性
- 方法數(shù)據(jù)——每個(gè)方法: 名稱腾么、返回類型、參數(shù)類型(按順序)杈湾、修飾符哮翘、屬性
- 方法代碼——每個(gè)方法: 字節(jié)碼、操作數(shù)棧大小毛秘、局部變量大小、局部變量表阻课、異常表叫挟;異常表中的每個(gè)異常處理程序:起始點(diǎn)、結(jié)束點(diǎn)限煞、處理程序代碼的程序計(jì)數(shù)器偏移量抹恳、捕獲異常類的常量池索引
2.2 堆(線程共享)
這也是一個(gè)共享資源(每個(gè) JVM 只有1個(gè)堆)。 所有對(duì)象及其對(duì)應(yīng)的實(shí)例變量和數(shù)組的信息都存儲(chǔ)在堆中署驻。 由于方法區(qū)和堆是線程共享的奋献,存儲(chǔ)在方法區(qū)和堆中的數(shù)據(jù)不是線程安全的。 堆是 GC 的一個(gè)很好的目標(biāo)旺上。
2.3 棧(線程獨(dú)有)
這不是一個(gè)共享的資源瓶蚂。 對(duì)于每個(gè) JVM 線程,當(dāng)線程啟動(dòng)時(shí)宣吱,將創(chuàng)建一個(gè)單獨(dú)的運(yùn)行時(shí)棧來存儲(chǔ)方法調(diào)用窃这。 對(duì)于每個(gè)方法調(diào)用,都會(huì)創(chuàng)建一個(gè)條目并將其添加(推送)到運(yùn)行時(shí)棧的頂部征候,這個(gè)條目稱為棧幀杭攻。
每個(gè)棧幀都具有局部變量表、 操作數(shù)棧和一個(gè)類的運(yùn)行時(shí)常量池的引用疤坝,這個(gè)類就是正在執(zhí)行的方法所屬的類兆解。 編譯時(shí)確定局部變量表和操作數(shù)棧的大小。 因此跑揉,根據(jù)該方法可以確定棧幀的大小锅睛。
當(dāng)方法正常返回或者在方法調(diào)用期間拋出未捕獲的異常時(shí),棧幀被移除(彈出)历谍。 還要注意衣撬,如果發(fā)生任何異常,stack trace 的每一行(如 printStackTrace()這樣的方法所示)表示一個(gè)棧幀扮饶。 棧是線程安全的具练,因?yàn)樗皇枪蚕碣Y源。
棧幀可分為三個(gè)部分:
局部變量表:它的索引從0開始甜无。 對(duì)于一個(gè)特定的方法扛点,涉及的局部變量相應(yīng)的值存儲(chǔ)在這里哥遮。 下標(biāo)0處存儲(chǔ)該方法所屬的類實(shí)例的引用。 從1開始陵究,保存該方法的入?yún)ⅰ?在方法參數(shù)確定后眠饮,保存方法中的局部變量
操作數(shù)棧:它充當(dāng)運(yùn)行時(shí)工作區(qū)來保存任何中間操作結(jié)果。 每個(gè)方法在操作數(shù)棧和局部變量表之間交換數(shù)據(jù)铜邮,并推入或彈出其他方法的調(diào)用結(jié)果仪召。 在編譯期間可以確定所需的操作數(shù)棧空間的大小
幀數(shù)據(jù):(譯者注:常量池引用等)所有與該方法相關(guān)的符號(hào)都存儲(chǔ)在這里松蒜。 對(duì)于異常扔茅,catch 塊信息也將保存在幀數(shù)據(jù)中
由于這些是運(yùn)行時(shí)棧幀,線程終止后秸苗,其棧幀也將被 JVM 銷毀召娜。
棧大小可以是動(dòng)態(tài)的或固定的。 如果線程需要比允許的更大的棧惊楼,則拋出 StackOverflowError玖瘸。 如果一個(gè)線程需要一個(gè)新的棧幀,并且沒有足夠的內(nèi)存來分配它檀咙,那么就會(huì)拋出 OutOfMemoryError雅倒。
2.4 PC寄存器(線程獨(dú)有)
對(duì)于每個(gè) JVM 線程,當(dāng)線程啟動(dòng)時(shí)弧可,將創(chuàng)建一個(gè)單獨(dú)的 PC 寄存器(程序計(jì)數(shù)器)屯断,以保存當(dāng)前正在執(zhí)行的指令的地址(方法區(qū)中的內(nèi)存地址)。 如果當(dāng)前的方法是 native 的侣诺,那么 PC 是未定義的殖演。 一旦執(zhí)行結(jié)束,PC 寄存器將用下一條指令的地址進(jìn)行更新年鸳。
2.5 本地方法棧(線程獨(dú)有)
在 Java 線程和本機(jī)操作系統(tǒng)線程之間有一個(gè)直接的映射趴久。 在為一個(gè) Java 線程準(zhǔn)備好所有狀態(tài)之后,還會(huì)創(chuàng)建一個(gè)單獨(dú)的本地棧搔确,以便存儲(chǔ)通過 JNI (Java本地接口)調(diào)用的本地方法信息(通常用 c/c++ 編寫)彼棍。
一旦創(chuàng)建并初始化了本機(jī)線程,它就會(huì)調(diào)用 Java 線程中的 run()方法膳算。 當(dāng) run()方法返回時(shí)座硕,處理未捕獲的異常(如果有的話) ,那么本機(jī)線程確認(rèn) JVM 是否需要由于線程終止而終止(即它是最后一個(gè)非 deamon 線程)涕蜂。 當(dāng)線程終止時(shí),本機(jī)線程和 Java 線程的所有資源都被釋放机隙。
一旦 Java 線程終止萨西,本機(jī)線程將被回收。 因此谎脯,操作系統(tǒng)負(fù)責(zé)調(diào)度所有線程并將它們分派到任何可用的 CPU。
3 執(zhí)行引擎
字節(jié)碼的實(shí)際執(zhí)行發(fā)生在這里源梭。 執(zhí)行引擎通過讀取上述運(yùn)行時(shí)數(shù)據(jù)區(qū)的數(shù)據(jù),逐行執(zhí)行字節(jié)碼中的指令废麻。
3.1 Interpreter
解釋器解釋字節(jié)碼并逐條執(zhí)行指令。 因此仲闽,它可以快速地解釋一行字節(jié)碼赖欣,但是執(zhí)行解釋結(jié)果是一個(gè)較慢的任務(wù)。 缺點(diǎn)是顶吮,當(dāng)一個(gè)方法被多次調(diào)用時(shí),每次都需要新的解釋和較慢的執(zhí)行悴了。
3.2 JIT 編譯器(Just-In-Time)
當(dāng)一個(gè)方法被多次調(diào)用時(shí),如果只有解釋器可用則每次解釋都會(huì)發(fā)生湃交,如何有效地處理這個(gè)冗余操作。 這在 JIT 編譯器中已經(jīng)成為可能藤巢。 首先,它將整個(gè)字節(jié)碼編譯為本地代碼(機(jī)器代碼)掂咒。 然后對(duì)于重復(fù)的方法調(diào)用,它直接提供本地代碼绍刮,使用本地代碼執(zhí)行比逐條解釋指令要快得多。 本地代碼存儲(chǔ)在緩存中孩革,因此可以更快地執(zhí)行。
然而膝蜈,即使對(duì)于 JIT 編譯器來說澈圈,編譯也比解釋器解釋要花費(fèi)更多的時(shí)間。 對(duì)于只執(zhí)行一次的代碼段帆啃,最好是解釋它瞬女,而不是編譯。 此外努潘,本地代碼存儲(chǔ)在緩存中诽偷,這是一個(gè)昂貴的資源。 在這些情況下疯坤,JIT 編譯器內(nèi)部檢查每個(gè)方法調(diào)用的頻率报慕,并決定只在選定的方法發(fā)生的次數(shù)超過一定級(jí)別時(shí)才編譯這個(gè)方法。 這種自適應(yīng)編譯的思想在 Oracle Hotspot 虛擬機(jī)中得到了應(yīng)用压怠。
3.3 GC(Garbage Collector)
只要對(duì)象被引用眠冈,JVM 就認(rèn)為它是活的。 一旦某個(gè)對(duì)象不再被引用菌瘫,應(yīng)用程序代碼無法訪問它蜗顽,垃圾收集器將刪除該對(duì)象并回收未使用的內(nèi)存。 一般來說雨让,垃圾收集發(fā)生在幕后雇盖,但是我們可以通過調(diào)用 System.gc()方法觸發(fā)它(同樣,執(zhí)行也不能保證)栖忠。
4 Java本地接口(JNI)
此接口用于與執(zhí)行所需的本地方法庫進(jìn)行交互崔挖,并提供本地庫的功能(通常用 c/c++ 編寫)。 這使 JVM 能夠調(diào)用 c/c++ 庫庵寞,也可由 c/c++ 庫調(diào)用狸相,這些庫用于特定的硬件。
5 本地方法庫
這是 c/c++ 本地庫的集合捐川,執(zhí)行引擎需要這些庫卷哩,可以通過提供的本機(jī)接口訪問它們。
JVM線程
我們討論了 Java 程序是如何執(zhí)行的属拾,但沒有特別提到執(zhí)行器将谊。 實(shí)際上,為了執(zhí)行我們前面討論過的每個(gè)任務(wù)渐白,JVM 并發(fā)運(yùn)行多個(gè)線程尊浓。 其中一些線程帶有應(yīng)用邏輯,由程序(應(yīng)用程序線程)創(chuàng)建纯衍,而其余的線程則由 JVM 本身創(chuàng)建栋齿,以執(zhí)行系統(tǒng)中的后臺(tái)任務(wù)(系統(tǒng)線程)。
主要的應(yīng)用程序線程是主線程,它是作為調(diào)用public static void main (String [])
的一部分創(chuàng)建的瓦堵,所有其他應(yīng)用程序線程都是由這個(gè)主線程創(chuàng)建的。 應(yīng)用程序線程執(zhí)行一些任務(wù)菇用,比如執(zhí)行以 main ()方法開始的指令,如果在任何方法邏輯中發(fā)現(xiàn)new
關(guān)鍵字杂穷,則在堆中創(chuàng)建對(duì)象等等卦绣。
主要的系統(tǒng)線程如下:
- Compiler threads:在運(yùn)行時(shí)滤港,由這些線程將字節(jié)碼編譯成本地代碼
- GC threads:所有與 GC 相關(guān)的活動(dòng)都由這些線程執(zhí)行
- Periodic task thread:定時(shí)器事件(即中斷)來調(diào)度周期性操作的執(zhí)行是由這個(gè)線程執(zhí)行的
- Signal dispatcher thread:這個(gè)線程接收發(fā)送到 JVM 進(jìn)程的信號(hào),并通過調(diào)用適當(dāng)?shù)?JVM 方法在 JVM 內(nèi)處理它們
- VM thread:作為前置條件山叮,有些操作需要 JVM 到達(dá)一個(gè)安全點(diǎn)樟凄,在這個(gè)點(diǎn)上堆的修改不再發(fā)生缝龄。 此類場(chǎng)景的示例包括
stop-the-world
垃圾收集挂谍、線程棧轉(zhuǎn)儲(chǔ)口叙、線程掛起和偏向鎖撤銷。 這些操作可以在一個(gè)稱為 VM 線程的特殊線程上執(zhí)行
原文引用
Understanding JVM Internals
JVM Internals
JVM Explained
The JVM Architecture Explained
How JVM Works — JVM Architecture?
Diffrenence between AppClassloader and SystemClassloader