我們一起聊聊JVM是如何執(zhí)行Java程序的
前言
如果你對JVM一知半解,如果你想了解JVM的工作流程,如果你知道一些JVM面試題卻無法將知識點串聯(lián)起來,那么這篇文章非常適合你。
從面試題說起
這些面試題Javaer們應該都很熟悉,但是你知道這些面試題的背后嗎?
- 你知道類加載機制嗎?
 - 什么是雙親委派機制?
 - 介紹一下JVM內存區(qū)域劃分
 - 堆為什么要分代設計?
 - 什么是內存的擔保機制?
 - 為什么Eden:S0:S1 比例是8:1:1?
 - 描述一下對象內存分配過程
 - 如何判斷對象已死?
 - 講一講內存模型?
 - 常用的JVM調優(yōu)參數有哪些?
 - 常用的垃圾回收算法有哪些?
 - 常用的垃圾收集器有哪些?
 - ......
 
圖片
如果你總是背了又忘,忘了又背,歸根結底,還是對JVM沒有一個系統(tǒng)的認識。
那么希望通過這篇文章,可以為你構建一個連貫的JVM框架。
JVM做了哪些事?
眾所周知,高級編程語言編寫的程序,最終要轉化為機器碼,才可以在計算機上運行。
圖片
“翻譯”的工作
我們在編寫完一段Java代碼后,如果想要運行它,需要通過Java編譯器,將其編譯為JVM認識的字節(jié)碼文件。
圖片
然后執(zhí)行Java命令,這段代碼就會通過JVM運行。
圖片
不僅僅“翻譯”
在這個過程中,JVM就充當了轉換的角色,負責將字節(jié)碼,翻譯成對應平臺上的機器指令。這樣的話,Java程序就可以在任何安裝了JVM的平臺上運行。這就是Java語言一次編寫到處運行的跨平臺特性。
圖片
翻譯字節(jié)碼的工作,是由JVM的執(zhí)行引擎完成。
在將字節(jié)碼翻譯為機器指令之前,JVM還有一個非常重要的工作,那就是將字節(jié)碼文件中的二進制數據準確的加載到JVM中。這個工作是由JVM的類加載系統(tǒng)完成,
另外,為了在運行時方便管理內存,JVM定義了一個專門的區(qū)域,也就是大名鼎鼎的運行時數據區(qū)。
圖片
所以,類加載系統(tǒng)、運行時數據區(qū)、執(zhí)行引擎,就構成了JVM平臺。
接下來,看一下它們是如何工作的。
在這之前,要對字節(jié)碼現(xiàn)有一個認識,畢竟它貫穿了Java代碼運行的整個流程。
Java虛擬機對Java編程語言一無所知,只知道一種特定的二進制格式,即類文件格式。類文件包含Java虛擬機指令(或字節(jié)碼)和符號表,以及其他輔助信息。
JVM 各部件如何協(xié)同工作?
類加載器先工作
類加載系統(tǒng)目的很明確,就是將字節(jié)碼文件中的二進制數據準確地加載到JVM,從Class文件加載到內存 & 對數據進行校驗、轉換解析和初始化,最終形成可被虛擬機直接使用的Java使用類型
執(zhí)行Java命令后,Java虛擬機啟動,類加載系統(tǒng)就開始工作了。
圖片
類加載系統(tǒng)首先會讀取指定的類文件,并遵循雙親委派機制進行加載。
圖片
然后將文件中的常量池、字段、方法和指令等數據加載到JVM內存的共享區(qū)域方法區(qū)中。
圖片
然后對其進行驗證,目的是為了確保類的正確性。比如版本號為52或更高時,不應該存在這個版本不支持的指令。
圖片
或者標識類文件的魔術數字是不是cafebabe,這些完整性的檢查和約束都是非常有必要,就像我們自己開發(fā)的應用,也不可能隨便讓別人訪問一樣。
圖片
驗證完成后,在方法區(qū)為類的靜態(tài)變量分配內存并設置默認值。
圖片
緊接著,將常量池中表示對象的符號引用,指向到實際的內存地址,也就是直接引用。
圖片
什么是符號引用呢?
符號引用是常量池中的類、方法、字段等指向的目標在字節(jié)碼文件中的靜態(tài)表示,當JVM運行時,需要將目標的靜態(tài)表示轉換成實際的內存指針,也就是直接引用。在這個例子中,如果JVM需要加載Object這個類,它會查找常量池中的#3(Class類型,指向#27),然后解析#27中的字符串java/lang/Object/為實際的類文件路徑,并加載這個類。
最后執(zhí)行靜態(tài)代碼塊,為靜態(tài)變量設置初始值,類加載工作就算完成了。
整個加載過程就是面試被經常問到的類加載機制。
圖片
那么問題來了:靜態(tài)變量為什么要先設置默認值,再設置初始值,知道的評論區(qū)留言。
執(zhí)行引擎開始工作
執(zhí)行引擎工作模式
靜態(tài)代碼塊被執(zhí)行時,執(zhí)行引擎就會處理這些指令。執(zhí)行引擎有兩種工作模式:
- 解釋執(zhí)行
 - 即時編譯
 
解釋執(zhí)行就是每次執(zhí)行都會逐行解釋字節(jié)碼指令
圖片
即時編譯是將熱點代碼,編譯成當前平臺的機器碼,并緩存下次就可以直接執(zhí)行機器碼,這樣就可以提高執(zhí)行效率。
圖片
JVM通常采用解釋器與即時編譯器并存的混合模式。在程序啟動時,解釋器可以立即發(fā)揮作用,省去編譯時間;隨著程序運行時間的推移,JIT編譯器逐漸發(fā)揮作用,將越來越多的熱點代碼編譯為本地機器碼,以提高執(zhí)行效率。
Main方法什么時候被執(zhí)行?
靜態(tài)代碼塊執(zhí)行完成后,JVM會繼續(xù)調用main方法。如果執(zhí)行Java命令的字節(jié)碼文件中沒有main方法,JVM就會報錯,這個是JVM規(guī)范。
圖片
運行時數據區(qū)域開始工作
執(zhí)行引擎工作期間,會和運行時數據區(qū)域有大量的交互。
線程私有的空間
調用main方法時,會創(chuàng)建一個線程并在運行時數據區(qū)中分配線程私有的空間:棧幀以及程序計數器。
圖片
程序計數器初始時會指向第一條指令, 然后隨著指令的執(zhí)行而遞增。
圖片
執(zhí)行靜態(tài)變量賦值的指令時,會把整數推送到棧幀中的操作數棧,隨后賦值給靜態(tài)變量。
圖片
在執(zhí)行創(chuàng)建一個Object實例的指令時,如果Object Class未被加載,類加載器會啟動加載過程。然后在堆中分配一塊內存并初始化實例。
圖片
大名鼎鼎的堆內存
分配內存這個過程,就涉及到“堆內存分代設計”、“對象內存分配過程”、“內存分配方式”等知識點了。
圖片
如果對象過多導致空間不足,JVM就會通過垃圾回收來釋放一些空間?!叭绾未_定對象是垃圾”、“使用哪個垃圾回收器”、“用了什么回收算法”就需要我們去了解。
圖片
實例初始化后,會將對象的引用存儲到局部變量表中。這樣的話,線程就可以通過引用訪問到該對象。
圖片
就這么一直工作
后續(xù)的代碼會延續(xù)這個流程,該加載類的加載類、該翻譯指令的翻譯、該分配內存的分配、該回收垃圾的回收,直到Java虛擬機停止工作。
            
                
                    
                        
圖片















 
 
 


















 
 
 
 