新四季網

jvm整體架構講解(諾依曼計算機體系)

2023-06-14 23:37:07

前言

在開始前,先說為什麼我從馮·諾依曼計算機體系,追溯到了JVM,一切原來如此。因為一顆剽悍種子發現的一個問題,就是多數人對待知識總想問是什麼,卻常常很少問為什麼(或者說知道了是什麼後也很少再問為什麼),正如這篇所要暢談的JVM,看了很多文章"什麼是JVM",我其實楞是沒搞懂什麼是JVM,或者說為什麼JVM是要這樣子呢?

直到回到知識歷史的起點,再來看現在知識,一切原來如此。

(理解知識背後一整條脈絡遠比純粹記住硬邦邦知識更為深刻,因為前者會帶來思考上的愉悅,而後者只會讓人喪失樂趣。)

誰說的?我一顆剽悍的種子眼裡飽含淚水說的。

所以我回到馮·諾依曼的計算機體系再到作業系統去追溯 JVM 來看看這整條脈絡。

回顧計算機發展歷史,我們都知道被稱為「現代計算機之父」的馮·諾依曼在1945提出並奠定到至今的馮·諾依曼計算機體系,也就是計算機結構。所以要知道一個真相是到至今為止不管在物理機之上構建的是什麼樣的作業系統、還是作業系統上構建什麼樣的虛擬機,或者再上層的應用,追溯到源頭都離不開這套體系下。(通俗的說我們被籠罩在這套體系之下)

(所以為什麼你總能在JVM虛擬機身上看到計算機結構熟悉的身影。)

計算機結構思想

在計算機結構中由 輸入設備、控制器、運算器、存儲器、輸出設備 這五大部分組成。而 控制器 運算器 就是我們再熟悉不過的CPU(中央處理器),存儲器就是內存、磁碟等,輸入設備例如:鍵盤、滑鼠,輸出設備如:顯示器等。而整個計算機結構的運行如下圖所示:

從上面可以看到,整個硬體已經布好了局,當一個程序被執行時,程序會被當成數據一樣,所有組成結構都可以被看成數據/指令流、控制流(圖下方箭頭所示),程序就像數據(或者指令)一樣,只要經過流就可以被牢牢操控。這也正是馮·諾依曼的思想 「程序應該像數據一樣可以被存儲」

作業系統的本質

在計算機結構之上還有一層,那就是作業系統,作業系統可以說是虛擬機的鼻祖,因為作業系統目的就是:如何操控物理機器。也就是如何將現實的物理資源(CPU、內存、磁碟)虛擬化成一臺能被你通過盯著屏幕、敲著鍵盤、劃著滑鼠就能操控現實的物理資源。

不錯,作業系統本質就是將 「物理資源虛擬化」!

(想想元宇宙的起點是不是從作業系統這裡開始了呢?)

在我們最常用的作業系統有WindowsLinux這兩種,不過這兩個作業系統之間是不兼容的,那麼就會存在編寫的程序並不能同時運行在兩個作業系統上,而開發程序時就得要分別開發。那麼就會觸發人類(程式設計師)的第一生產力

千萬別笑!人類發明蒸汽機以來,就從未停止過釋放重複勞動力的工作,人類發明洗衣機替代洗衣、洗碗機替代洗碗、汽車替代馬車,以及再到20世紀30年代就提出,但至今仍在火的自動駕駛即將替代司機。但是它們並不具備通用性,就像問鼎了圍棋界的谷歌機器 AlphaGo ,就算再厲害也只會下圍棋這一件事(圍棋曾被認為是計算機沒法逾越的天花板,但被AlphaGo戰勝,不過AlphaGo也只是屬於弱人工智慧),可是從整個人類科技發展來看,人類正在一步步開發著具備更通用性的人工智慧,也就是有一天超越強人工智慧而跨越起點的超人工智慧,相信未來程式設計師唯一能做可能就是一段對話:

程式設計師:「 嘿 Siri*:幫我開發一個掘金,順便幫我安排一個叫 一顆剽悍的種子 的傢伙寫一篇JVM。」

程式設計師:「嘿,對了,Siri,看文章前別忘了給那傢伙一鍵三連。」

Siri:「f*ck 又提需求。」

程式設計師:「什麼?」

Siri:「安排」

(扯遠了題外話,希望你能Get到人類追求通用性(懶)遠不止在JVM中體現。)

JVM的全稱Java Virtual Machine也就是java虛擬機,拋開Java虛擬機前綴Java,虛擬機其實就是在「虛擬(抽象)計算機」,也就是在作業系統之上再次虛擬出一臺計算機,來屏蔽不同硬體和作業系統之間的差異(如果說作業系統是用戶與物理資源之間的橋梁,那麼JVM就是不同硬體和作業系統上的橋梁),JVM目的就是具備通用性,也就是我們常說的Java可以跨平臺開發。

那麼回到JVM這時容易懂了,JVM可以像計算機結構一樣去運行所編寫的程序,而JVM主要由:類裝載器、運行時數據區、執行引擎、本地庫接口這四個部分所構成。

我們知道一個可運行的程序其實不過是我們所定義的一個個 .java原始碼文件組成(這些文件其實就是我們程式設計師對於事物的抽象,然後交由計算機幫我們實現)。而JVM只規定.class字節碼文件格式,這樣的好處很明顯,那就是不僅只是Java可以運行在JVM上,而是只要是能生成.class字節碼文件也都可以運行在JVM之上,所以Java虛擬機並不是Java語言專有的。所以JVM通過字節碼存儲格式統一了所有平臺,字節碼是構成平臺無關性的基石。如:Python、Ruby、Scala等語言(可以說JVM野心真大!)。

所以.java文件想要在JVM上被執行,就必須先由javac編譯器轉成後.class再交給Class Loader 類裝載器去進行類的加載等。而這也相當於開始像計算機結構一樣的輸入設備正式的進去到整個機器內部。如下圖所示:

類裝載器執行過程

說到虛擬機類裝載器,就不得不說類加載的機制,而一套機制就一定會有一個過程,也就是一個類進入入口(類裝載器)到虛擬機中必經的整個生命周期:Loading 加載、Linking 連接、Initialization 初始化、Using 使用、Unlading 卸載。如下圖所示:

但其中最主要的Class文件裝載過程是加載、連接、初始化這三步。

加載

類加載過程的第一個過程毫無疑問是加載,如果虛擬機還沒有加載過此類,會通過類加載器將字節碼文件加載到內存中。當然也可以是從ZIP壓縮包,或者JAR、WAR等格式,最終也不過是從中取出類文件而已。

連接

雖然任何二進位都可以是Class類型,但是只有JVM能夠裝載的Class文件類型才能運行在JVM之上。(也就是要符合虛擬機的規範的字節碼文件才能通過)加載後是連接的過程(連接通俗的說就是將類文件與虛擬機建立關聯),從上圖可以看到連接的包括了三個過程:驗證、準備、解析。 連接的第一步是 驗證,驗證會從四個階段的檢驗依次進行:

驗證

驗證的四個階段:

文件格式驗證,其實就是檢查是否符合Class文件規範,主要有:魔數檢查、版本檢查等等,例如:魔數檢查就是看Class文件開頭是否是 0xCAFEBABE 開頭(魔數通俗的說就是打一個讓JVM認識的標籤)。版本檢查就是看Class文件主次版本號是否能在當前版本虛擬機處理等等。元數據驗證,(元數據可能不好理解,其實元數據從字面意思上就是 描述數據的數據,所以元數據驗證就是語義的檢查)檢查類中是否被final修飾、是否有繼承了父類等等。字節碼驗證,要做的就是確定程序中的語義是否合法且符合邏輯的(通過分析數據流和控制流),確保跳轉指令指向正確位置,操作數類型合理等等。符號引用驗證,先說什麼是符號引用?符號引用是以一組符號來描述所引用的目標,可以是任何字面量。(聽到這你可能還不能Get到的話,暖男一顆剽悍的種子,下面舉個例子,你就悟了)

在上面我們知道 .class 文件是經過編譯器編譯後的文件,而 .class 文件裡面的內容就是字節碼,通過字節碼中記錄著自己將要使用的其他類或方法等。

例如下面這段代碼,我們定義一個字符串類型變量的 userName,通過 System.out.println 列印。

String userName System.out.println(userName);

我們看上面代碼編譯成字節碼後是什麼樣子的,如下所示:

0 ldc #2 2 astore_1 3 getstatic #3 6 aload_1 7 invokevirtual #4 10 return

可以看到在代碼中最常見的表達式:

System.out.println(userName)

而轉換後的字節碼中是使用符號引用來「代替」表達:

invokevirtual #4

所以符號引用驗證就是通過檢查符號引用所「代替」的類、屬性、方法是否存在且有權限被訪問。

上面的驗證東西很多,但是不管什麼樣的驗證,都是為了 確保類符合 Java 規範與符合 JVM 規範,同時 避免危害到虛擬機的安全

準備

連接的第二步是 準備,這個階段主要做兩件事,為通過驗證了的類來分配內存空間設置初始化

如對於 final static 修飾的變量,會直接賦值我們的定義值。可以看下面這段代碼,會在準備階段分配內存,並初始化值。

private final static String value = "一顆剽悍的種子"

各數據類型默認初始值,如下圖所示:

注意:上圖中並沒有 boolean 類型,Java中的 boolean 類型的底層實現實際上就是 int 類型,int 類型默認值 0,對應的就是 boolean 類型默認值 false。)

解析

連接的第三步是 解析,解析階段的工作就是將 符號引用轉為直接引用。因為在編譯時類、方法等都是用符號引用來代替(所以為什麼叫符號引用,符號只是個標識),而符號引用是並不知道這些數據所引用的 實際地址

所以如果僅僅用符號引用就面臨一個問題,就是 不能確定一定存在該對象。所以通過解析將符號引用轉化為 JVM可直接獲取的內存地址或指針,也就是 直接引用

當解析將符號引用轉成直接引用時,也就是目標必定已經在虛擬機的內存中存在(說白了用直接引用就是確定了存在該類、方法或屬性)。

初始化

類裝載過程中最後階段是初始化。而這個階段將會執行構造器方法,它是在通過我們前面提到過的Javac將 .java 文件編譯成 .class 字節碼文件時,所有類初始化代碼,也就是包括靜態變量賦值語句、靜態代碼塊、靜態方法,收集在一起後成為 方法。

簡單的概括初始化目的就是 初始化給類靜態變量或靜態代碼塊為程式設計師自己所定義的值

到此,類的加載過程就像馮·諾依曼計算機結構中的輸入設備,負責將數據丟進了入口後就是真正到JVM內部(JVM運行時數據區)去操縱數據,直至將我們的想法通過代碼最後交給機器來完成。

JVM運行時數據區

JVM運行時數據區主要分為 堆、程序計數器、方法區、虛擬機棧和本地方法棧 這五個分區。其中按 線程共享線程私有 兩類:

線程共享:堆、方法區。線程私有:程序計數器、虛擬機棧、本地方法棧。

在JVM 內存中最大的一塊內存空間就是堆,而且堆也被所有線程共享,所以堆也幾乎存儲著所有的對象。堆被按年代進行劃分為 新時代、 老年代 以及 持久代,新生代又接著被分為 Eden (伊甸園區) 和 Surivor (倖存區)。而 Surivor 進一步由 From Survivor 和 To Survivor 進行劃分。

JDK 8之前以及之後堆按年代劃分的變化,如下圖所示:

方法區

方法區跟堆一樣是線程共享的區域,當.class字節碼文件在JVM加載時會被分配到不同的數據結構,如常量池、方法、構造函數,同時也主要包括用來存儲已被虛擬機加載的類相關信息(類信息又包括了類的版本、欄位、方法、接口和父類等信息)都存放在方法區。

程序計數器

我們知道Java程序是多線程執行的,所以即想要能滿足多個線程的交叉執行,又想要確保多個線程都能完整的執行完各自的工作,那麼一旦出現被中斷的線程,線程執行到哪條的內存地址(指令)就必須被保存下來,這樣當被中斷的線程恢復時就又可以接著執行下去。

而這就是程序計數器的工作,用來記錄哪個線程當前執行到哪條指令。所以分支、循環、跳轉、異常處理、線程恢復等都需要依賴計數器完成。(形象的說就是程序控制流的指示器)

虛擬機棧

虛擬機棧是線程私有的區域,所以不用關心數據一致性問題。當我們創建一個線程時,同樣在JVM中也會創建一個與之對應的棧,稱為虛擬機棧。

而虛擬機棧的內部其實是一個或多個的棧幀,每一個棧幀又都對應著一個Java方法的調用。

其運行過程是,當我們創建一個新方法同時,與之對應會在虛擬機棧中同樣創建一個新的棧幀(當前棧幀)會被放在棧頂(只要是棧就會有棧頂和棧底),同時程序計數器也會指向這個當前棧地址。如下圖所示:

在每個棧幀裡又存儲著方法的 局部變量表、操作數棧、動態連結、方法返回地址、附加信息 參與著方法的調用和返回。

所以 如果說堆解決了數據存儲的問題,那麼棧就是解決了程序如何運行的問題。

本地方法棧

本地方法棧是為了運行JVM本地方法(也就是 Native 方法)而準備的空間。而從字面上之所以被稱為本地方法棧,也是因為 Natice 方法很多也都是由C語言所實現的。

如果說 「類加載子系統是計算機結構中的輸入設備,那麼運行時數據區就是計算機結構中的CPU(控制器和運算器)和存儲器,那麼最後的輸出設備就是執行引擎。」

執行引擎

到了執行引擎,可以說是JVM最後的一個環節,從最先的一個Java源文件編譯成.class字節碼文件經過了類加載子系統,也經過了上面的JVM運行時數據區,經過了這一整系列下來,通俗的說字節碼文件已經被重新打碎重組成了一個可以由JVM所操控的一系列數據(再回看計算機結構,數據成流,被布好的局安排的明明白白),但是問題是代碼並不能被執行。因為JVM並不是將高級語言直接轉成機器指令,而是字節碼,所以字節碼的真正的運行得由執行引擎去將字節碼翻譯成機器指令後,由真正物理機去執行。

但是我們知道JVM只是對計算機的抽象,它的一切都只是建立在軟體層面自行實現的。而物理計算機只認識機器碼指令,這些機器碼指令運行通過處理器、緩存、指令集和作業系統等構建了物理機的執行引擎。

所以JVM想要讓Java程序運行起來,同樣也是要在 「虛擬(通俗說是模擬)一個執行引擎」,而執行引擎的目的就是 將字節碼指令解釋(編譯)為機器指令

因為回到本質,真正幹活做事的是物理機,所以執行引擎就是將字節碼轉成為物理機可執行的機器碼(從用戶看執行引擎就是一臺翻譯機)

我們使用的HotSpot VM是目前虛擬機的代表之一,它是集解釋器和JIT即時編譯器於一身 的架構。也就說Java虛擬機運行時,解釋器JIT(just in time 即時編譯器) 互相協作。

解釋器

在JVM早期使用的就是解釋器(大多數語言同樣也是),解釋器就是在運行時逐行解釋字節碼轉化成機器碼再執行程序。(這也解釋了上面所說,JVM為什麼不直接將Java語言直接轉化為機器指碼指令直接就能在物理機上執行。而是要通過加多一層字節碼文件來具備通用性,所以以這種 「加一個翻譯器」的方式,來避免高級語言直接轉成本地機器指令的耦合,重要的事情不要忘了,JVM虛擬機是一個概念,也不要忘了目的是具備通用性)

在Java發展路程中,從最早期的,也是最古老的字節碼解釋器。之後到了目前普遍使用的模板解釋器。一共有兩套解釋執行器。字節碼解釋器是在執行時通過純軟體代碼模擬字節碼的執行,所以效率也非常低。而模板解釋器是將每一條字節碼和模板函數相關聯,而模板函數能直接產生這條字節碼執行時的機器碼,從而達到提高解釋器性能。

但是單憑字節碼解釋器效率還遠不夠,所以 為了追求一把即時速度的推背感,虛擬機又加上了JIT也就是即時編譯器

JIT 即時編譯器

從上面我們知道在JVM執行引擎擁有字節碼解釋器之後又加入了JIT,而使用JIT對字節碼轉化為機器碼指令時,關注的核心一點就是 程序中運行時被調用頻繁的代碼,被稱為 「熱點代碼」

而要找到這些 「熱點代碼」就需要使用到JIT的 熱點探測。目前的Host Spot 的JVM採用的熱點探測是基於 計數器熱點探測。計數器熱點探測很好理解,就是統計每個方法執行次數,當超過認為的熱點閾值,那麼就屬於「熱點代碼」。

計數器熱點探測被分為 方法調用計數器回邊計數器 兩類。方法調用計數器用來統計代碼調用次數,而回邊計數器則用來統計循環執行次數。

方法調用的計數器除了遞增,也同樣有熱度衰減,也就是當代碼調用次數超過一定時間已經不足提交給JIT,那麼調用計數器會遞減。

而回邊計數器的主要目的是為了觸發 OSR (On StackReplacement)棧上編譯。在一些循環周期較長代碼會在循環時間內,會直接將代碼替換執行緩存機器碼。

本地方法接口與本地方法庫

在JVM中如果需要與一些底層系統實現交互,那麼就會使用到本地方法接口與本地方法庫,也就是 Native Method,本地方法接口與本地方法庫其目很簡單,就是借用到C或C 等其他語言的資源。

總結

我們從馮諾依曼計算機體系理解了計算機結構思想 程序應該像數據一樣可以被存儲,也就是說程序就像數據(或者指令)一樣,只要經過像組件構建的流就可以被牢牢操控。而接著探究作業系統本質其實就是將物理資源虛擬化,可以說是用戶與物理資源之間的橋梁。最後追溯到JVM其實就是在一臺虛擬(抽象)的計算機,如果類加載子系統是計算機結構中的輸入設備,那麼運行時數據區就是計算機結構中的CPU(控制器和運算器)和存儲器,那麼到最終的輸出設備就是執行引擎。

到此,JVM還有類加載的雙親委派機制,以及JVM的垃圾回收機制、JVM的堆棧等異常以及JVM配置參數等內容沒有聊,因為這些內容都值得再獨立拎出來細說。所以這一篇文章可以當成JVM的一個開篇,也可以說是JVM的一張地圖。地圖作用不是告訴你應該去哪的路標,而是能縱覽整個全貌後,至於你對這整個知識是想怎麼理解的都取決於的是你自己的思考。

(最後,我們至今還沒有非馮·諾依曼體系下的新計算機結構,但不妨大膽設想未來,也許會在距實用還相去甚遠的量子計算機上看到呢。)

原文創作:https://juejin.cn/post/7098500546297856030

,
同类文章
葬禮的夢想

葬禮的夢想

夢見葬禮,我得到了這個夢想,五個要素的五個要素,水火只好,主要名字在外面,職業生涯良好,一切都應該對待他人治療誠意,由於小,吉利的冬天夢想,秋天的夢是不吉利的
找到手機是什麼意思?

找到手機是什麼意思?

找到手機是什麼意思?五次選舉的五個要素是兩名士兵的跡象。與他溝通很好。這是非常財富,它擅長運作,職業是仙人的標誌。單身男人有這個夢想,主要生活可以有人幫忙
我不怎麼想?

我不怎麼想?

我做了什麼意味著看到米飯烹飪?我得到了這個夢想,五線的主要土壤,但是Tu Ke水是錢的跡象,職業生涯更加真誠。他真誠地誠實。這是豐富的,這是夏瑞的巨星
夢想你的意思是什麼?

夢想你的意思是什麼?

你是什​​麼意思夢想的夢想?夢想,主要木材的五個要素,水的跡象,主營業務,主營業務,案子應該抓住魅力,不能疏忽,春天夢想的吉利夢想夏天的夢想不幸。詢問學者夢想
拯救夢想

拯救夢想

拯救夢想什麼意思?你夢想著拯救人嗎?拯救人們的夢想有一個現實,也有夢想的主觀想像力,請參閱週宮官方網站拯救人民夢想的詳細解釋。夢想著敵人被拯救出來
2022愛方向和生日是在[質量個性]中

2022愛方向和生日是在[質量個性]中

[救生員]有人說,在出生88天之前,胎兒已經知道哪天的出生,如何有優質的個性,將走在什麼樣的愛情之旅,將與生活生活有什么生活。今天
夢想切割剪裁

夢想切割剪裁

夢想切割剪裁什麼意思?你夢想切你的手是好的嗎?夢想切割手工切割手有一個真正的影響和反應,也有夢想的主觀想像力。請參閱官方網站夢想的細節,以削減手
夢想著親人死了

夢想著親人死了

夢想著親人死了什麼意思?你夢想夢想你的親人死嗎?夢想有一個現實的影響和反應,還有夢想的主觀想像力,請參閱夢想世界夢想死亡的親屬的詳細解釋
夢想搶劫

夢想搶劫

夢想搶劫什麼意思?你夢想搶劫嗎?夢想著搶劫有一個現實的影響和反應,也有夢想的主觀想像力,請參閱週恭吉夢官方網站的詳細解釋。夢想搶劫
夢想缺乏缺乏紊亂

夢想缺乏缺乏紊亂

夢想缺乏缺乏紊亂什麼意思?你夢想缺乏異常藥物嗎?夢想缺乏現實世界的影響和現實,還有夢想的主觀想像,請看官方網站的夢想組織缺乏異常藥物。我覺得有些東西缺失了