Java是怎么運(yùn)行起來的?深入理解Java虛擬機(jī)
Java虛擬機(jī)(JVM)是一個(gè)運(yùn)行時(shí)環(huán)境,可以執(zhí)行用Java編程語言編寫的程序。Java語言是一種高級(jí)語言,它通過抽象和封裝的機(jī)制,讓開發(fā)者可以專注于業(yè)務(wù)邏輯和功能實(shí)現(xiàn),而不用關(guān)心底層的細(xì)節(jié)。因此,運(yùn)行/開發(fā)Java程序時(shí),不必深入了解Java程序的執(zhí)行過程或JVM的內(nèi)部原理。對于大多數(shù)開發(fā)者來說,JVM就像一個(gè)神奇的盒子,能夠幫助他們實(shí)現(xiàn)功能和完成任務(wù)。
但是,了解JVM是如何支持Java語言和其他相關(guān)語言的,對于程序員來說是很有裨益的!
本文分享一下Java的工作原理和JVM的內(nèi)部結(jié)構(gòu)。
1. Java虛擬機(jī)
Java虛擬機(jī)(JVM)是一個(gè)抽象的機(jī)器,用來執(zhí)行一種代碼,即bytecode。你可以把它看作是我們的代碼和計(jì)算機(jī)硬件之間的橋梁,它把我們的代碼作為輸入,轉(zhuǎn)換成字節(jié)碼并在計(jì)算機(jī)硬件上運(yùn)行它,從而實(shí)現(xiàn)開發(fā)者預(yù)期的結(jié)果。
2. 字節(jié)碼 (bytecode)
字節(jié)碼是一種JVM能夠理解的文件類型。它是通過compilingJava代碼(使用javac)生成的一種Java程序的中間表示形式。它之所以叫字節(jié)碼,是因?yàn)槊總€(gè)操作碼(operation)都是單字節(jié)大小的。字節(jié)碼可以再次編譯成機(jī)器碼并在計(jì)算機(jī)上運(yùn)行。
3. 編譯
運(yùn)行Java程序的第一步是編譯。如果你有一個(gè)單獨(dú)的Java文件,你可以使用提供的命令行工具javac來觸發(fā)編譯。
javac HelloWorld.java
上面代碼會(huì)把一個(gè)給定的Java文件編譯成.class文件,其中包含bytecode。如果源代碼有錯(cuò)誤,編譯會(huì)失敗并報(bào)出編譯錯(cuò)誤。
你可以使用提供的工具javap來查看已創(chuàng)建的類文件,以了解類文件的內(nèi)部情況。
javap HelloWorld.class
4. 執(zhí)行
在通過編譯創(chuàng)建了.class文件之后,可以使用java語法來啟動(dòng)一個(gè)JVM的實(shí)例,它會(huì)觸發(fā)一個(gè)包含多個(gè)復(fù)雜步驟的執(zhí)行路徑,最終執(zhí)行我們提供的代碼。
java HelloWorld
首先JVM需要獲取.class文件,并將它加載到JVM的內(nèi)存區(qū)域中。這個(gè)初始過程是通過JVM類加載器來實(shí)現(xiàn)的。
5. 什么是類加載?
抽象地說,類加載就是掃描并遍歷提供的.class文件,并將類文件中的內(nèi)容加載到JVM的內(nèi)存區(qū)域中。然后,執(zhí)行引擎就可以引用這些存儲(chǔ)的數(shù)據(jù),繼續(xù)執(zhí)行我們的代碼。
JVM中有三種類型的類加載器,分別是:
- 引導(dǎo)類加載器
- 擴(kuò)展類加載器
- 應(yīng)用類加載器
引導(dǎo)類加載器的職責(zé)是加載基礎(chǔ)/核心的Java類,這些類對于Java程序運(yùn)行是必不可少的。在早期的Java版本中,這些核心類被包含在位于jre/lib目錄下的rt.jar文件中,但在后來的Java版本中,rt.jar中的內(nèi)容被分割成模塊化的組件。
擴(kuò)展類加載器的職責(zé)是加載lib/ext目錄下的類,這些類可能包括我們在代碼中使用的任何擴(kuò)展。
應(yīng)用類加載器是三種中最常用的一種,它負(fù)責(zé)加載用戶定義的類。它會(huì)掃描我們程序的類路徑,并加載其中的類。
6. 類加載過程
類加載過程有兩個(gè)主要步驟:
- 加載
- 鏈接
7. 加載
在加載過程中,類加載器讀取類文件的二進(jìn)制表示形式,即.class文件,并在JVM的運(yùn)行時(shí)內(nèi)存中創(chuàng)建它的表示。這個(gè)表示稱為Class Object,它位于JVM內(nèi)存的方法區(qū)中。
8. 鏈接
在加載過程之后,開始鏈接。鏈接有三個(gè)步驟。
- 驗(yàn)證 — 確保類文件的正確性。驗(yàn)證類是否符合Java規(guī)范。
- 準(zhǔn)備 — 為靜態(tài)塊/字段分配內(nèi)存,并為靜態(tài)變量賦予默認(rèn)值(不是初始值?。?/li>
- 解析 — 解析類文件中的(符號(hào))引用。
解析:
在鏈接的解析階段,類加載器會(huì)解析常量池表,這是一個(gè)位于.class文件/類對象中的實(shí)體,類似于一個(gè)符號(hào)表,指定了類中的字段/方法/引用。在類文件中,對其他類的引用是以符號(hào)方式表示的,沒有具體的內(nèi)存地址來引用。解析會(huì)搜索JVM內(nèi)存,并為那些符號(hào)引用分配具體的引用。如果在.class文件中發(fā)現(xiàn)了一個(gè)尚未加載的類,它會(huì)觸發(fā)該類本身的加載/鏈接過程,這可能會(huì)導(dǎo)致一個(gè)遞歸的加載和鏈接過程。
在字節(jié)碼加載和鏈接之后,類就成功地存儲(chǔ)在JVM內(nèi)存中(將在后面的部分討論),并準(zhǔn)備好初始化。
9. 初始化
當(dāng)代碼中第一次用new關(guān)鍵字或靜態(tài)字段來引用一個(gè)類,或者當(dāng)程序執(zhí)行時(shí)遇到一個(gè)初始化類(比如Main類),則會(huì)觸發(fā)類文件的初始化。
在初始化階段,執(zhí)行靜態(tài)塊,靜態(tài)變量被分配初始值。
10. 運(yùn)行時(shí)內(nèi)存區(qū)域
在上面的段落中,多次提到了將類文件數(shù)據(jù)存儲(chǔ)在JVM內(nèi)存中。這些數(shù)據(jù)究竟存儲(chǔ)在哪里,來作為加載/鏈接/初始化的結(jié)果?答案是運(yùn)行時(shí)內(nèi)存區(qū)域。
JVM運(yùn)行時(shí)內(nèi)存區(qū)域是指定的內(nèi)存空間,它被劃分為多個(gè)部分,用于存儲(chǔ)執(zhí)行相關(guān)/類文件相關(guān)的數(shù)據(jù)。
運(yùn)行時(shí)內(nèi)存區(qū)的主要區(qū)域如下:
(1) 方法區(qū)
方法區(qū)是運(yùn)行時(shí)內(nèi)存的一部分,用于存儲(chǔ)與類文件相關(guān)的數(shù)據(jù)。運(yùn)行時(shí)常量池、字段元數(shù)據(jù)、類元數(shù)據(jù)、方法元數(shù)據(jù)和字節(jié)碼本身等都存儲(chǔ)在方法區(qū)中。
(2) 程序計(jì)數(shù)器(PC)
程序計(jì)數(shù)器是一個(gè)小的內(nèi)存區(qū)域,用于存儲(chǔ)當(dāng)前正在執(zhí)行的操作的地址,這是Java程序執(zhí)行的必要信息。每個(gè)線程都有自己的PC。
(3) 堆
存儲(chǔ)所有的類/數(shù)組實(shí)例,是所有線程共享的一塊內(nèi)存。
(4) JVM棧
保存局部變量和部分結(jié)果。包含棧幀。每個(gè)線程都有自己的JVM棧。
11. 棧幀
當(dāng)一個(gè)方法被調(diào)用時(shí),在棧中創(chuàng)建一個(gè)新的幀。它會(huì)存儲(chǔ)與該方法相關(guān)的局部變量和部分結(jié)果。如果在該方法內(nèi)部調(diào)用了另一個(gè)方法,就會(huì)為新調(diào)用的方法創(chuàng)建一個(gè)新的棧幀。在給定線程中,一次只有一個(gè)幀是活動(dòng)的。
12. 執(zhí)行
在上面的部分中,簡要地介紹了Java源代碼是如何編譯并加載到JVM運(yùn)行時(shí)內(nèi)存區(qū)域中的。
接下來看看這些數(shù)據(jù)是如何執(zhí)行的。
這部分過程是通過JVM的執(zhí)行引擎來實(shí)現(xiàn)的,它由兩個(gè)主要部分組成:(執(zhí)行引擎還包括許多其他組件,但在本文中不會(huì)提及。)
- 解釋器
- JIT(即時(shí))編譯器
“Java作為一種編程語言,是一種混合的解釋和編譯語言,也就是說Java代碼既要經(jīng)過編譯,又要經(jīng)過解釋。簡單來說,當(dāng)類文件開始運(yùn)行時(shí),JVM會(huì)先用解釋器直接執(zhí)行字節(jié)碼,不需要編譯。這樣做的主要好處是可以提高啟動(dòng)速度和執(zhí)行速度(不用等待編譯過程)。
在解釋的過程中,JVM會(huì)發(fā)現(xiàn)代碼中的熱點(diǎn)和熱區(qū),也就是經(jīng)常執(zhí)行或者可以優(yōu)化的代碼段。這些代碼段會(huì)被JIT編譯器編譯成本地代碼,然后執(zhí)行引擎會(huì)從解釋模式切換到執(zhí)行模式?!?/p>
這個(gè)編譯過程有多個(gè)層次,稱為分層編譯。