文章為作者原創(chuàng)坪稽,轉(zhuǎn)載請注明出處曼玩,多謝配合!
本文打算針對jvm執(zhí)行java代碼流程做個簡單的梳理窒百。對jvm有個大框架的認識黍判。
首先通過一張整體流程圖來宏觀了解下jvm執(zhí)行java代碼流程:
下面拆解開來分別進行解讀:
一.編譯
java編譯器:比如javac (sun公司編譯器,jdk默認自帶的編譯器)
java編譯器的作用:讀入java源代碼篙梢,進行語法校驗顷帖,通過后生成中間代碼即字節(jié)碼(.class文件)。字節(jié)碼文件是一種和任何具體機器環(huán)境及操作系統(tǒng)環(huán)境無關(guān)的中間代碼,它是一種二進制文件窟她。編譯器編譯生成與平臺無關(guān)的字節(jié)碼文件后,提供給 JVM (Java虛擬機)執(zhí)行蔼水。
另外需要注意的是:
(1)編譯器編譯一個java文件震糖,涉及到的對象都會單獨生成一一對應(yīng)的.class文件,有多少對象生成多少個趴腋。
(2)字節(jié)碼 ≠ 機器碼 吊说,字節(jié)碼是虛擬機認識的碼,機器碼是操作系統(tǒng)認識的碼优炬。
C/C++在編譯的時候直接編譯成機器碼颁井,而java是先編譯成字節(jié)碼,再由虛擬機轉(zhuǎn)換為機器碼蠢护。
詳細了解字節(jié)碼雅宾,可參考:http://www.importnew.com/24088.html
(3)為什么java編譯出來的是字節(jié)碼而不是機器碼?
最主要的目的是跨平臺葵硕,為了實現(xiàn)跨平臺眉抬,就決定了不能像 c,c++ 那樣直接把源代碼編譯成可執(zhí)行文件懈凹,因為不同cpu蜀变,不同操作系 統(tǒng)的指令封裝格式是不一樣的。java編譯成的字節(jié)碼文件.class介评,與硬件和操作系統(tǒng)無關(guān)库北,這是跨平臺基礎(chǔ),然后具體執(zhí)行们陆,再用各自平臺解釋器寒瓦,解釋成本地機器碼。java是一種編譯+解釋的語言棒掠。
二.類裝載器如何把.class文件裝載到內(nèi)存
在編譯期孵构,所有的*.java文件被編譯成.class文件。在運行期烟很,class文件只有被加載到j(luò)vm內(nèi)存中才能運行颈墅。這個裝載工作是由類裝載器完成的。實質(zhì)就是把class文件從硬盤讀取到內(nèi)存中雾袱,并對數(shù)據(jù)進行驗證恤筛、準備、解析芹橡、初始化毒坛,最終形成可以被jvm直接使用的java類型。
1.裝載方式以及裝載器介紹:
類裝載方式,有兩種
(1)隱式裝載煎殷, 程序在運行過程中當碰到通過new 等方式生成對象時屯伞,隱式調(diào)用類裝載器加載對應(yīng)的類到j(luò)vm中,
(2)顯式裝載豪直, 通過class.forname()等方法劣摇,顯式加載需要的類。
Java類的加載是動態(tài)的弓乙,它并不會一次性將所有類全部加載后再運行末融,而是保證程序運行的基礎(chǔ)類(像是基類)完全加載到j(luò)vm中,至于其他類暇韧,則在需要的時候才加載勾习。這當然就是為了節(jié)省內(nèi)存開銷。
Java的類加載器有三個懈玻,對應(yīng)Java的三種類:(java中的類大致分為三種: 1.系統(tǒng)類 2.擴展類 3.由程序員自定義的類 )
Bootstrap Loader // 負責(zé)加載系統(tǒng)類 (指的是內(nèi)置類巧婶,像是String,對應(yīng)于C#中的System類和C/C++標準庫中的類)
|
- - ExtClassLoader // 負責(zé)加載擴展類(就是繼承類和實現(xiàn)類)
|
- - AppClassLoader // 負責(zé)加載應(yīng)用類(程序員自定義的類)
jdk源碼角度的加載過程不做過多解釋了酪刀,有興趣的可以參看:[https://blog.csdn.net/architect0719/article/details/50411545](https://blog.csdn.net/architect0719/article/details/50411545)
2.JVM類加載機制
?全盤負責(zé):當一個類加載器負責(zé)加載某個Class時粹舵,該Class所依賴的和引用的其他Class也將由該類加載器負責(zé)載入,除非顯示使用另外一個類加載器來載入骂倘。
?雙親委派:雙親委派模型的工作流程是:如果一個類加載器收到了類加載的請求眼滤,它首先不會自己去嘗試加載這個類,而是把請求委托給父加載器去完成历涝,依次向上诅需,因此,所有的類加載請求最終都應(yīng)該被傳遞到頂層的啟動類加載器中荧库,只有當父加載器在它的搜索范圍中沒有找到所需的類時堰塌,即無法完成該加載,子加載器才會嘗試自己去加載該類分衫。
?緩存機制:緩存機制將會保證所有加載過的Class都會被緩存场刑,當程序中需要使用某個Class時,類加載器先從緩存區(qū)尋找該Class蚪战,只有緩存區(qū)不存在牵现,系統(tǒng)才會讀取該類對應(yīng)的二進制數(shù)據(jù),并將其轉(zhuǎn)換成Class對象邀桑,存入緩存區(qū)瞎疼。這就是為什么修改了Class后,必須重啟JVM壁畸,程序的修改才會生效贼急。
- 類加載的過程:
類裝載器就是尋找類或接口字節(jié)碼文件進行解析并構(gòu)造JVM內(nèi)部對象表示的組件茅茂,在java中類裝載器把一個類裝入JVM,經(jīng)過以下步驟:
加載太抓、驗證空闲、準備、解析走敌、初始化五個階段进副。
加載:
1)通過一個類的全限定名來獲取定義此類的的二進制字節(jié)流
2)將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)運行時的數(shù)據(jù)結(jié)構(gòu)
3)在內(nèi)存的堆區(qū)生成一個java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口悔常。
驗證:確保Class文件中的字節(jié)流中的包含信息符合jvm的要求,并且不會虛擬機自身的安全给赞。
準備:在方法區(qū)給靜態(tài)的類變量分配內(nèi)存机打,并設(shè)置初始值。實例變量(未被static修飾的類變量)將會在對象實例化時片迅,隨對象一起分配到j(luò)ava堆中残邀。
解析:將符號引用轉(zhuǎn)成直接引用。
初始化:對靜態(tài)變量和靜態(tài)代碼塊執(zhí)行初始化工作芍锦。
三文捶、一個類在jvm內(nèi)存中數(shù)據(jù)是如何被管理的
首先我們知道壁熄,jvm的一個重要職責(zé)就是管理好從計算機內(nèi)存空間申請來的一畝三分地,運行時數(shù)據(jù)區(qū)常見劃分方式為:
1)程序計數(shù)器:一個指針空免,指向執(zhí)行引擎正在執(zhí)行的指令的地址;
2)虛擬機棧:局部變量的基本數(shù)據(jù)類型和引用盆耽;
3)堆:引用的對象實體蹋砚、成員變量全部存儲與堆中(包括基本數(shù)據(jù)類型,引用和引用的對象實體)摄杂;
4)方法區(qū):加載類的信息坝咐、靜態(tài)變量;里面包含了常量池析恢,常量池里放常量以及串池墨坚;
5)本地方法棧:它的存儲跟虛擬機棧類似,只是針對的是native方法映挂。
(其中堆和方法區(qū)是線程共享的泽篮,其余的則是線程隔離的)
有一個小結(jié)論:
局部變量的基本數(shù)據(jù)類型和引用存儲于棧中,引用的對象實體存儲于堆中袖肥。
——因為它們屬于方法中的變量咪辱,生命周期隨方法而結(jié)束。
成員變量全部存儲與堆中(包括基本數(shù)據(jù)類型椎组,引用和引用的對象實體)
——因為它們屬于類油狂,類對象終究是要被new出來使用的。
jvm的gc針對的主要就是堆區(qū):gc的詳細分析參考之前的文章:http://www.reibang.com/p/220d6827be0d
四.執(zhí)行引擎
1.執(zhí)行引擎是干嘛的
前面我們了解了,類裝載器裝載編譯后的字節(jié)碼专筷,并加載到運行時數(shù)據(jù)區(qū)弱贼,但是我們知道,Java字節(jié)碼是用一種人類可以讀懂的語言編寫的磷蛹,而不是用機器可以直接執(zhí)行的語言吮旅,所以需要由執(zhí)行引擎執(zhí)行這些字節(jié)碼,轉(zhuǎn)換生成由機器碼組成的可被jvm執(zhí)行的文件味咳。
執(zhí)行引擎找到入口main方法來執(zhí)行其中的字節(jié)碼庇勃。
2.轉(zhuǎn)換方式
(1)解釋器:一條一條地讀取,解釋并且執(zhí)行字節(jié)碼指令槽驶。因為它一條一條地解釋和執(zhí)行指令责嚷,所以它可以很快地解釋字節(jié)碼,但是執(zhí)行起來會比較慢掂铐。這是解釋執(zhí)行的語言的一個缺點罕拂。字節(jié)碼這種“語言”基本來說是解釋執(zhí)行的。
(2)即時(Just-In-Time)編譯器:Oracle Hotspot VM使用一種JIT編譯器全陨,即時編譯器被引入用來彌補解釋器的缺點爆班。執(zhí)行引擎首先按照解釋執(zhí)行的方式來執(zhí)行,然后在合適的時候辱姨,即時編譯器把整段字節(jié)碼編譯成本地代碼柿菩。然后,執(zhí)行引擎就沒有必要再去解釋執(zhí)行方法了雨涛,它可以直接通過本地代碼去執(zhí)行它碗旅。執(zhí)行本地代碼比一條一條進行解釋執(zhí)行的速度快很多。編譯后的代碼可以執(zhí)行的很快镜悉,因為本地代碼是保存在緩存里的祟辟。不過,用JIT編譯器來編譯代碼所花的時間要比用解釋器去一條條解釋執(zhí)行花的時間要多侣肄。因此旧困,如果代碼只被執(zhí)行一次的話,那么最好還是解釋執(zhí)行而不是編譯后再執(zhí)行稼锅。
因此吼具,內(nèi)置了JIT編譯器的JVM都會檢查方法的執(zhí)行頻率,如果一個方法的執(zhí)行頻率超過一個特定的值的話矩距,那么這個方法就會被編譯成本地代碼拗盒。
具體做法:
每個方法被調(diào)用一次就給這個方法計數(shù)加1。
那些方法的被調(diào)用計數(shù)越多锥债,JVM就優(yōu)先翻譯成機器碼(使用JIT編譯器)陡蝇,當然少的可能在執(zhí)行到的時候再翻譯(使用解釋器)
(3)AOT(Ahead-Of-Time)編譯器 IBM 在IBM JDK 6里不僅引入了JIT編譯器痊臭,它同時還引入了AOT(Ahead-Of-Time)編譯器。它使得多個JVM可以通過共享緩存來共享編譯過的本地代碼登夫。簡而言之广匙,通過AOT編譯器編譯過的代碼可以直接被其他JVM使用。除此之外恼策,IBM JVM通過使用AOT編譯器來提前把代碼編譯器成JXE(Java EXecutable)文件格式來提供一種更加快速的執(zhí)行方式鸦致。