通過編譯器將源代碼編譯稱為字節(jié)碼文件(包含程序運行各類信息的數(shù)據(jù)文件),字節(jié)碼文件被類加載器加載到內(nèi)存中,在內(nèi)存中將各類數(shù)據(jù)分類整理媒熊,最后在解釋器的作用下帽哑,將程序運行起來谜酒。
一、字節(jié)碼規(guī)范
JVM不關(guān)心類文件的來源妻枕,可源于java 僻族、JRuby、Groovy程序等屡谐;并且類文件并非依賴于磁盤文件述么,可以只存在內(nèi)存中。
class文件跟xml一樣是用來存儲數(shù)據(jù)的愕掏。類文件由“無符號數(shù)”度秘、”表“組成。類文件規(guī)范如下:
ClassFile {
u4 magic; //Class 文件的標志
u2 minor_version;//Class 的小版本號
u2 major_version;//Class 的大版本號
u2 constant_pool_count;//常量池的數(shù)量
cp_info constant_pool[constant_pool_count-1];//常量池
u2 access_flags;//Class 的訪問標記
u2 this_class;//當前類
u2 super_class;//父類
u2 interfaces_count;//接口
u2 interfaces[interfaces_count];//一個類可以實現(xiàn)多個接口
u2 fields_count;//Class 文件的字段屬性
field_info fields[fields_count];//一個類可以有多個字段
u2 methods_count;//Class 文件的方法數(shù)量
method_info methods[methods_count];//一個類可以有個多個方法
u2 attributes_count;//此類的屬性表中的屬性數(shù)
attribute_info attributes[attributes_count];//屬性表集合
}
存放的數(shù)據(jù)和位置:
- 字面量和符號引用存放在常量池中:cp_info饵撑;
- 類的訪問標志(private剑梳、public等)存放在:access_flags;
- 類信息(全限定名)存放在:this_class滑潘、super_class
- 字段信息(字段作用域垢乙、是否static、final众羡、volatile侨赡、transient、數(shù)據(jù)類型粱侣、字段名稱)存放在field_info羊壹;
- 方法信息存放在:method_info。注意方法的具體代碼(字節(jié)碼指令)是存放在attribute_info的Code屬性中齐婴。
常量池:17種常量類型且每種類型有完整獨立的數(shù)據(jù)結(jié)構(gòu)油猫,每一項都是一個表,互相間沒有共性無聯(lián)系柠偶,每個數(shù)據(jù)結(jié)構(gòu)的第一位都是標識位情妖。
屬性表集合:虛擬機規(guī)范一共制定了29項;與常量池類似诱担,每個屬性都有自己的存儲結(jié)構(gòu)毡证。
根據(jù)虛擬機的字節(jié)碼文件引出以下問題:
1. 虛擬機如何將源代碼編譯為字節(jié)碼?
2. 虛擬機如何字節(jié)碼文件加載到內(nèi)存中蔫仙?
3. 字節(jié)碼加載到內(nèi)存后料睛,如何讓程序運行起來呢?
二、編譯階段
- 虛擬機如何將源代碼編譯為字節(jié)碼文件恤煞?
將源代碼編譯為字節(jié)碼是由編譯器完成的屎勘; 根據(jù)編譯期的不同分為:前端(javac)、即時(C1和C2)和提前(Jaotc)編譯器居扒。提前編譯器是直接把程序編譯成與目標機器指令集相關(guān)的二進制代碼概漱。
前端編譯器
需要注意的是前端編譯器如Javac,對代碼的運行效率幾乎沒有任何優(yōu)化措施,因為虛擬機設(shè)計團隊選擇把對性能的優(yōu)化集中到運行期喜喂,這樣可以讓那些不是由javac產(chǎn)生的字節(jié)碼文件(如JRuby瓤摧、Groovy程序)也能享受到編譯器優(yōu)化所帶來的性能提升。但是javac這類編譯器可以通過“語法糖”提高編碼效率夜惭。
“語法糖”:目的是提高代碼開發(fā)效率姻灶。
以下是Java支持的語法糖:
- 泛型:Java采用的是“類型擦除式泛型”,即泛型只存在于源碼中诈茧,編譯后的字節(jié)碼中全部泛型被替換成為原來的裸類型产喉,并在相應(yīng)的地方插入了強制轉(zhuǎn)型代碼。在運行期間ArrayList<Integer>和ArrayList<String>是一個類型(方法重載時這兩個類型屬于同一個類型敢会,編譯報錯)曾沈。這樣做的缺點有兩個:1.運行期間無法得到泛型類型信息,會讓代碼變得繁瑣鸥昏。2.泛型擦除后塞俱,導(dǎo)致ArrayList<int>這種類型不支持,因為基礎(chǔ)數(shù)據(jù)類型不能與Object互轉(zhuǎn)吏垮。
- lambda表達式:不算純粹的語法糖障涯,但是在前端編譯器中做了大量的轉(zhuǎn)換工作。
- 其他語法糖:自動裝箱膳汪、拆箱唯蝶;循環(huán)遍歷;變長參數(shù)遗嗽;條件編譯粘我;內(nèi)部類;枚舉類痹换;斷言語句征字;數(shù)值字面量;對枚舉和字符串的switch支持娇豫;try語句中定義和關(guān)閉資源匙姜。
以Javac為例的前端編譯器:虛擬機規(guī)范對編譯的約束相當寬松,極端情況下會出現(xiàn)在javac中可以編譯但是在IDE中不能編譯冯痢。編譯主要分為:
1 解析與填充符號表:
1.1 詞法分析:是將源代碼的字符(程序編寫的最小單位)流轉(zhuǎn)變?yōu)闃擞洠ǔ绦蚓幾g的最小單位)集合的過程氮昧。
1.2 語法分析:根據(jù)標記序列構(gòu)造抽象語法樹或详,抽象語法樹每個節(jié)點都代表一個語法結(jié)構(gòu),例如包郭计、類型、運算符椒振、接口昭伸、注釋都可以是一種特定的語法結(jié)構(gòu)。
1.3 完成詞法和語法分析后澎迎,需要對符號表進行填充庐杨;符號表是由一組符號地址和符號信息構(gòu)成的數(shù)據(jù)結(jié)構(gòu),類似于哈希表中鍵值對的存儲形式夹供。2 注解處理器執(zhí)行:注解在設(shè)計上原本與代碼一樣只會在運行期間起作用灵份,但在JDK6后提出“插入式注解處理器”的API,可以將注解提前在編譯期對代碼中的特定注解進行處理哮洽√钋可以將“插入式注解處理器”看作是一組編譯器的插件,這些插件工作時允許讀取鸟辅、修改和添加抽象語法樹的任意元素氛什。如插件Lombok就是利用“插入式注解處理器”API干預(yù)編譯器的行為。
3 語義分析和字節(jié)碼生成:
3.1 語義分析:抽象樹能夠表示一個正確的源程序匪凉,但無法保證程序的語義符合邏輯枪眉。語義分析就是對結(jié)構(gòu)上正確源程序進行上下文相關(guān)性質(zhì)的檢查。
3.2 語法糖解析:語法糖避免的代碼的“啰嗦”和減少語法“錯誤”再层,但是在編譯期間還是需要將“語法糖”恢復(fù)原來的語法贸铜。javac中通過desugar()完成解析。
3.3 字節(jié)碼生成:字節(jié)碼生成不見將前面步驟所生成的信息轉(zhuǎn)化成字節(jié)碼寫道磁盤聂受,同時還進行少量的代碼添加和轉(zhuǎn)化工作蒿秦。例如實例的構(gòu)造器<init>()。
即時編譯器
按照虛擬機規(guī)范饺饭,即時編譯器和提前編譯器都是非必需的渤早。
即時編譯器:用到即時編譯器的時候,虛擬機已經(jīng)完成類的加載過程瘫俊,當字節(jié)碼文件進入內(nèi)存后鹊杖,Java程序最初先是通過解釋器執(zhí)行的,當虛擬機發(fā)現(xiàn)某個方法或代碼塊運行的特別頻繁扛芽,就認定這些代碼是“熱點代碼”(多次被調(diào)用和多次被執(zhí)行的循環(huán)體)骂蓖,為了提高熱點代碼的效率,虛擬機通過即時編譯器進行代碼優(yōu)化(會改變字節(jié)碼文件)川尖。這種編譯方式因為發(fā)生在方法執(zhí)行過程中登下,被形象的稱為“棧上編譯”。
即時編譯器在HotSpot中內(nèi)置了三個,其中“客戶端編譯器(C1)”和“服務(wù)端編譯器(C2)”存在很久被芳,JDK10后出現(xiàn)了Graal編譯器缰贝。
在分層編譯模式之前,虛擬機都是采用一個解釋器一個編譯器搭配的工作(混合)模式畔濒;但是也可以通過設(shè)置是否采用混合模式剩晴。分層模式之后解釋器、客戶端編譯器(C1)和服務(wù)端編譯器(C2)會同時工作侵状,熱點代碼被多次編譯赞弥,用客戶端編譯器獲取更高的編譯速度,用服務(wù)端編譯器獲取更好的編譯質(zhì)量趣兄。
代碼被標記為熱點代碼的方式有兩種:
- 基于采樣的熱點探測:虛擬機周期性的檢查各個線程的調(diào)用棧頂绽左,如果發(fā)現(xiàn)某個方法經(jīng)常出現(xiàn)在棧頂,那么這個方法就是熱點代碼艇潭。
- 基于計數(shù)器的熱點探測:虛擬機會為每個方法建立計數(shù)器拼窥,如果某個方法執(zhí)行次數(shù)超過閾值,那么這個方法就是熱點代碼蹋凝。HotSpot為每個方法準備“方法調(diào)用計數(shù)器”和“回邊計數(shù)器”闯团,當?shù)竭_閾值后就會觸發(fā)編譯器進行優(yōu)化。
編譯器如何進行熱點代碼的優(yōu)化仙粱?
在后臺執(zhí)行編譯的過程房交,前端編譯器利用簡單快速的三段式編譯器,進行局部(如常量傳播伐割、空值檢查消除)優(yōu)化候味。而后端編譯器會執(zhí)行大部分經(jīng)典的優(yōu)化動作:無用代碼消除、循環(huán)展開隔心、循環(huán)表達式外移白群、消除公共子表達式、常量傳播硬霍、基本塊重排序帜慢、范圍檢查消除、空值檢查消除等唯卖。
提前編譯器
按照虛擬機規(guī)范粱玲,即時編譯器和提前編譯器都是非必需的。
提前編譯在JDK1.0時候就可以使用外掛的提前編譯拜轨。但是提前編譯在很長時間內(nèi)沒有進展和應(yīng)用抽减;直到2013,在Android世界中出現(xiàn)了ART使用提前編譯橄碾,把使用即時編譯的Dalvik徹底終結(jié)卵沉。提前編譯器才大展身手颠锉。
三、加載階段
- 虛擬機如何字節(jié)碼文件加載到內(nèi)存中史汗?
通過類加載將字節(jié)碼的數(shù)據(jù)加載到內(nèi)存中琼掠。類的加載分為:加載、連接(驗證停撞、準備眉枕、解析)、初識化怜森、使用、和卸載谤牡。其中解析可不按照該順序副硅。
類的加載過程:
- 加載:通過類的全限定名來獲取定義該類的二進制流;然后將字節(jié)碼中所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)換為方法區(qū)運行時的數(shù)據(jù)結(jié)構(gòu)翅萤。注意:數(shù)據(jù)已經(jīng)從字節(jié)碼轉(zhuǎn)移到內(nèi)存中的方法區(qū)了恐疲。
- 驗證:驗證文件格式、驗證元數(shù)據(jù)套么、字節(jié)碼驗證(Code屬性的數(shù)據(jù))培己、最后是符號引用驗證。
- 準備:將類中定義的變量分配到方法區(qū)并初始化胚泌;這里分配僅僅包括類變量(static修飾的)省咨,而不包括實例變量,實例變量會在對象實例化的時候隨著對象一起分配到堆內(nèi)存中玷室。
- 解析:將字節(jié)碼中常量池中的符號引用替換直接引用零蓉。
- 初始化:在準備階段變量被賦〇值,在初始化階段中會根據(jù)程序員通過程序編碼制定主觀計劃去初始化類變量和其他資源穷缤。
類的加載器
對于任意一個類敌蜂,都必須由加載它的加載器+類本身確定在虛擬機中的唯一性。
對于虛擬機來說只有兩類加載器:啟動類加載器和其他類加載器津肛。在程序員角度Java一直保持著三層類加載器章喉、雙親委派的類加載架構(gòu)。
啟動類加載器:負責加載JAVA_HOME\lib下的類秸脱。
擴展類加載器:負責加載JAVA_HOME\lib\ext下的類。
應(yīng)用程序加載器:負責加載用戶類路徑下的所有類部蛇。
雙親委派:一個類加載器收到加載類的請求后撞反,首先將請求委派給父類加載器,每個層次的類加載器都是如此搪花;最終請求會被傳給啟動類加載器遏片,只有當父類加載器反饋無法完成這個加載請求時嘹害,子加載器才會嘗試自己去加載該類。這樣的好處是Object在程序的各類中都能保證是同一個類吮便。
四笔呀、運行階段
- 字節(jié)碼加載到內(nèi)存后,如何讓程序運行起來呢髓需?
- 將字節(jié)碼中的數(shù)據(jù)分類放入內(nèi)存中的不同區(qū)域许师。
- 虛擬機解釋器解釋棧中的指令碼,利用棧中的“操作數(shù)椓糯遥”執(zhí)行的指令(入棧進行計算微渠,出棧則存入局部變量表)。