摘要:字節(jié)碼生成把語法樹定義的抽象的語法結(jié)構(gòu)按照二進(jìn)制字節(jié)碼的規(guī)則排布成字節(jié)碼,最終我們可以看到滿足虛擬機(jī)運(yùn)行要求的二進(jìn)制字節(jié)碼被轉(zhuǎn)換出來。上面的過程完成后,命令扮演的編譯器就將源代碼轉(zhuǎn)成了結(jié)構(gòu)化的二進(jìn)制字節(jié)碼。
這篇文章的素材來自周志明的《深入理解Java虛擬機(jī)》。
作為Java開發(fā)人員,一定程度了解JVM虛擬機(jī)的的運(yùn)作方式非常重要,本文就一些簡單的虛擬機(jī)的相關(guān)概念和運(yùn)作機(jī)制展開我自己的學(xué)習(xí)過程,是這個(gè)系列的第四篇。
Java字節(jié)碼的編譯生成我們討論完了字節(jié)碼的結(jié)構(gòu)和活化字節(jié)碼在執(zhí)行引擎下的執(zhí)行之后要回到字節(jié)碼的原點(diǎn):java的字節(jié)碼是怎么形成的呢?
我們這里討論的僅僅是從程序員編寫的java源代碼的編譯得到的字節(jié)碼,但是要知道的事,字節(jié)碼不僅僅可以從源文件編譯生成,字節(jié)碼可以通過直接用二進(jìn)制的字節(jié)拼接產(chǎn)生,這個(gè)拼接的起點(diǎn)除了間接通過編譯期生成,也可以通過直接寫進(jìn)內(nèi)存,比如通過動態(tài)代理構(gòu)造的臨時(shí)代理類就是通過直接寫入內(nèi)存的二進(jìn)制字節(jié)碼形成的,再比如jsp通過jsp轉(zhuǎn)換器可以轉(zhuǎn)變?yōu)橐粋€(gè)對應(yīng)的請求處理類,等等。總之,我們在這里討論的僅僅是通過編譯期將靜態(tài)的java源代碼文件編譯成二進(jìn)制字節(jié)碼。
使用javac編寫的java命令是編譯過程的執(zhí)行者,這個(gè)命令的使命就是把java源文件轉(zhuǎn)換成為java二進(jìn)制字節(jié)碼,javac完成這一使命的步驟主要包括如下的子過程:
解析與填充符號表
插入式注解處理器的注解處理過程
分析與字節(jié)碼生成
這個(gè)過程的詳細(xì)數(shù)據(jù)流和控制流如下:
這些過程的目的和一般的傳統(tǒng)的編譯過程類似,因?yàn)楹蛡鹘y(tǒng)編譯過程的源文件到機(jī)器代碼的目的相比,java源代碼到虛擬機(jī)二進(jìn)制字節(jié)碼的編譯過程只是最終運(yùn)行的平臺是虛擬機(jī),除此之外大致是一樣的處理辦法。
解析這個(gè)過程是以源代碼為輸入流,詞法分析器和語法分析器為控制器,抽象語法樹為輸出流,最終生成的語法樹是一個(gè)以各種語法節(jié)點(diǎn)(接口、包等)為頂層節(jié)點(diǎn)的樹結(jié)構(gòu),詞法分析器對輸入流轉(zhuǎn)換成詞法元單位Token的序列,語法分析器對Token序列進(jìn)行分析得到最終的語法樹,順著這個(gè)語法樹的各個(gè)頂層節(jié)點(diǎn),可以找到程序中所有的變量、方法甚至是注釋的各種信息。語法樹是后期語義分析的基礎(chǔ)。
一個(gè)語法樹的實(shí)例:
在解析過后會分析生成的語法樹中的各類符號,包括程序中的各類符號的信息都將存儲在這個(gè)符號表里。在經(jīng)歷完這一步之后符號表將成為一個(gè)包含了語法樹頂層節(jié)點(diǎn)的表,順著這個(gè)表可以分類地尋到每個(gè)符號的信息。
注解處理過程在引入注解之后加入了對插入式注解處理器的編譯過程,注解是形如”@XX”的語法結(jié)構(gòu),這個(gè)語法結(jié)構(gòu)的目的當(dāng)然不是簡單的標(biāo)記,而是對應(yīng)到了一個(gè)對應(yīng)的注解處理器之上,注解完成的任務(wù)是注解處理器定義的,因此在這個(gè)過程里注解處理器定義的任務(wù)將會以修改語法樹的方式起作用。每次處理一個(gè)注解后都可能會改變語法樹的結(jié)構(gòu),然后再啟用符號表的填充,這個(gè)循環(huán)(round)將會是一次小規(guī)模的重建語法樹和符號表,當(dāng)掃描完所有的注解后語法樹的結(jié)構(gòu)在這個(gè)階段將會穩(wěn)定下來,然后給出一個(gè)為下面過程提供信息的To do List。實(shí)際上注解處理過程是程序員在編譯過程中控制程序的很少的機(jī)會,因?yàn)槠渌^程大都是是編譯器以無人為控制(沒有程序員編寫程序的指導(dǎo))的情況下的處理。
語義分析能通過詞法語法分析并不意味著語義上是成立的,因此這個(gè)過程是處理語義的過程,語義分析器通過對符號表索引的語法樹的分析,對程序表達(dá)的語義進(jìn)行分析。它包括幾個(gè)字過程:
標(biāo)注檢查:主要是類型對應(yīng)變量聲明以及常量折疊的檢查;
數(shù)據(jù)控制流檢查:對程序上下文邏輯的檢驗(yàn),包括局部變量賦值、返回值和異常處理等;
解語法糖:語法糖是程序員友好的語法規(guī)則,這些友好的規(guī)則還是要在這個(gè)過程中解開成為真正需要表達(dá)的意思的(裝箱拆箱、泛型、遍歷循環(huán)等的語法都會在底層替換稱為“復(fù)雜”的實(shí)現(xiàn))。
字節(jié)碼生成把語法樹定義的抽象的語法結(jié)構(gòu)按照class二進(jìn)制字節(jié)碼的規(guī)則排布成class字節(jié)碼,最終我們可以看到滿足虛擬機(jī)運(yùn)行要求的二進(jìn)制字節(jié)碼被轉(zhuǎn)換出來。在這個(gè)過程中還會有特定的代碼添加和初級的優(yōu)化,比如默認(rèn)的類構(gòu)造器
注意不是構(gòu)造函數(shù),構(gòu)造函數(shù)是在填充符號表的階段完成的,構(gòu)造函數(shù)用于完成new操作,而構(gòu)造器是在內(nèi)存中構(gòu)造出該類的基本結(jié)構(gòu),而構(gòu)造函數(shù)是語法層級較高的操作,同時(shí)還會將靜態(tài)代碼塊static{}加入類構(gòu)造器,將構(gòu)造代碼塊{}加入到實(shí)例構(gòu)造器中,包括實(shí)例變量和類變量的初始化、父類構(gòu)造器的調(diào)用等過程都會加入到構(gòu)造器中去。
上面的過程完成后,javac命令扮演的編譯器就將源代碼轉(zhuǎn)成了結(jié)構(gòu)化的二進(jìn)制字節(jié)碼。
Java字節(jié)碼的運(yùn)行優(yōu)化 解釋執(zhí)行字節(jié)碼的運(yùn)行過程我們在第三篇的時(shí)候已經(jīng)解釋過了:
Java虛擬機(jī) :Java字節(jié)碼指令的執(zhí)行
當(dāng)時(shí)我們看到的是逐一把二進(jìn)制命令執(zhí)行,也就是說執(zhí)行引擎每取一條二進(jìn)制指令就執(zhí)行一次,這種執(zhí)行方式稱為解釋執(zhí)行(interpreted mode),我們其實(shí)可以看出解釋執(zhí)行的優(yōu)點(diǎn)在于每次執(zhí)行的時(shí)候都會確知當(dāng)前程序的狀態(tài),但是每次執(zhí)行都要從方法區(qū)里取命令,然后再能夠在堆棧中執(zhí)行操作,每次都去取指令無疑是會減慢執(zhí)行速度的,即便把馬上要執(zhí)行的命令置于高速緩沖上。
即時(shí)編譯執(zhí)行基于這個(gè)弱點(diǎn)就有了另一種執(zhí)行模式,編譯模式(compiled mode),這個(gè)模式中非常重要的參與者就是JIT即時(shí)編譯器(Just Intime),編譯模式的原理其實(shí)就像是C一樣的編譯型語言一樣把源代碼直接編譯成機(jī)器語言然后一口氣運(yùn)行完,省去了每次取指令的時(shí)間(只不過C是直接把源代碼編譯成機(jī)器碼,而java是把二進(jìn)制字節(jié)碼通過虛擬機(jī)的JIT即時(shí)編譯器編成本地機(jī)器碼)。
JIT觸發(fā)的條件:
不是所有的代碼被以編譯模式執(zhí)行都是好的,因?yàn)镴IT編譯本身也是費(fèi)時(shí)的,所以必須在非常有必要進(jìn)行編譯的部分才應(yīng)該去編譯,這些地方就是需要反復(fù)使用的部分,因?yàn)榉磸?fù)使用的部分是需要進(jìn)行進(jìn)行最大化優(yōu)化的,而只用幾次的代碼可能使用的時(shí)間還不及JIT編譯的時(shí)間,這樣做就沒有“性價(jià)比”了。所以我們來看看被稱為hotspot的這些反復(fù)使用的代碼被編譯模式執(zhí)行的特點(diǎn):
如果是多次調(diào)用的方法或者是多次執(zhí)行的循環(huán)體就是hotspot的。
一般虛擬機(jī)會為每個(gè)方法添加一個(gè)計(jì)數(shù)器,這個(gè)計(jì)數(shù)器用于計(jì)量方法執(zhí)行的次數(shù),當(dāng)這個(gè)計(jì)數(shù)器計(jì)量這個(gè)方法調(diào)用超過某個(gè)閾值時(shí)就會觸發(fā)JIT編譯器對這個(gè)方法的編譯,即時(shí)編譯后的代碼會成為本地機(jī)器碼,執(zhí)行速度會大大加快,同時(shí)由于這個(gè)方法使用次數(shù)非常多,所以將會大大加速程序的運(yùn)行。當(dāng)然這個(gè)過程不是僅僅這么簡單,因?yàn)槿绻@樣的話程序運(yùn)行時(shí)間足夠長的話會有很多并不那么“熱”的代碼也會成為hotspot的,比如某段代碼運(yùn)行了一段時(shí)間后陷入了“冷”狀態(tài),那么這段代碼就算不上是hotspot的,因此默認(rèn)情況下虛擬機(jī)查看的更是代碼在一個(gè)時(shí)間內(nèi)的調(diào)用頻率,如果一段時(shí)間內(nèi)的使用次數(shù)足夠多才會說明這段代碼是hotspot的。
同樣的,循環(huán)體會被虛擬機(jī)加入一個(gè)回邊計(jì)數(shù)器用以統(tǒng)計(jì)循環(huán)體的使用頻率。
下面展示的就是在JIT這套機(jī)制下的編譯模式的執(zhí)行流程:
值得注意的是,JIT編譯的時(shí)候并不是說線程就停在這里一直等待編譯的本地機(jī)器碼的結(jié)果出現(xiàn),而是繼續(xù)以解釋模式執(zhí)行,這能充分利用執(zhí)行時(shí)間,等到下次執(zhí)行到這里的時(shí)候再看看是否JIT編譯已經(jīng)有了結(jié)果,如果有了就去執(zhí)行本地代碼,否則還得解釋執(zhí)行以繼續(xù)等待。
JIT即時(shí)編譯器在后臺執(zhí)行的編譯任務(wù)時(shí)也會首先對字節(jié)碼進(jìn)行優(yōu)化,包括方法內(nèi)聯(lián)和常量傳播等策略,然后轉(zhuǎn)換成高級中間代碼表示,再進(jìn)行一次優(yōu)化,然后轉(zhuǎn)為平臺相關(guān)的低級中間代碼表示,再進(jìn)行一次優(yōu)化,最后變成平臺相關(guān)的機(jī)器代碼。這個(gè)底層的優(yōu)化過程屬于相對機(jī)器層級的優(yōu)化。
這里所提的還有幾個(gè)編譯過程中的比較典型的優(yōu)化技術(shù):
公共子表達(dá)式消除:用于消除重復(fù)計(jì)算帶來的性能損失;
數(shù)組邊界檢查消除:編譯期確定的數(shù)組范圍將不必要的邊界檢查條件去除;
方法內(nèi)聯(lián):避免方法調(diào)用的時(shí)候產(chǎn)生的棧切換和現(xiàn)場恢復(fù)等過程帶來的損耗,由于java的因?yàn)樘摲椒ǖ闹剌d重寫等問題帶來的方法分派問題,內(nèi)聯(lián)的結(jié)果不能確定一定正確,所以才用的一般是激進(jìn)優(yōu)化失敗退回的策略;
逃逸分析:如果一個(gè)方法中的局部變量不會通過調(diào)用函數(shù)作為參數(shù)傳出被外部方法或線程使用的時(shí)候,可以采用更加高效的辦法優(yōu)化:
棧上分配:在棧上直接為變量對象分配空間,因?yàn)橹懒诉@個(gè)對象不會發(fā)生逃逸被外部訪問到,所以某種程度上來講這就是一個(gè)“臨時(shí)封閉在方法里”的對象,所以這種棧上分配的辦法不會造成問題。使用完畢后就將它直接釋放,也減小了gc的壓力。
同步消除:同理的,不會被外部線程訪問到的“臨時(shí)封閉在方法里”的對象是不會發(fā)生共享的,所以可以消除它的同步標(biāo)記。
標(biāo)量替換:如果一個(gè)局部變量對象是“臨時(shí)封閉在方法里”的對象,那么就完全沒有必要建立一個(gè)完整的對象,只需要在棧上創(chuàng)建它的相關(guān)字段就可以了,這樣做可以加速對真正被訪問的變量的速度。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/64702.html
學(xué)習(xí)JVM的相關(guān)資料 《深入理解Java虛擬機(jī)——JVM高級特性與最佳實(shí)踐(第2版)》 showImg(https://segmentfault.com/img/bVbsqF5?w=200&h=200); 基于最新JDK1.7,圍繞內(nèi)存管理、執(zhí)行子系統(tǒng)、程序編譯與優(yōu)化、高效并發(fā)等核心主題對JVM進(jìn)行全面而深入的分析,深刻揭示JVM的工作原理。以實(shí)踐為導(dǎo)向,通過大量與實(shí)際生產(chǎn)環(huán)境相結(jié)合的案例展示了解...
摘要:近期在閱讀最新幾版的官方文檔過程中發(fā)現(xiàn)不少術(shù)語不清之處特發(fā)此文總結(jié)以下的術(shù)語大量在官方文檔中直接出現(xiàn)且直接如基本詞語一樣使用不理解它們會嚴(yán)重影響閱讀自適應(yīng)自旋鎖自適應(yīng)自旋鎖是一個(gè)允許線程在特定點(diǎn)自旋等待特定事件發(fā)生而不是直接進(jìn)行并等待該事件 近期在閱讀JAVA最新幾版的官方文檔過程中發(fā)現(xiàn)不少術(shù)語不清之處,特發(fā)此文總結(jié).以下的術(shù)語大量在官方文檔中直接出現(xiàn),且直接如基本詞語一樣使用,不理解...
摘要:前言本文內(nèi)容基本摘抄自深入理解虛擬機(jī),以供復(fù)習(xí)之用,沒有多少參考價(jià)值。此區(qū)域是唯一一個(gè)在虛擬機(jī)規(guī)范中沒有規(guī)定任何情況的區(qū)域。堆是所有線程共享的內(nèi)存區(qū)域,在虛擬機(jī)啟動時(shí)創(chuàng)建。虛擬機(jī)上把方法區(qū)稱為永久代。 前言 本文內(nèi)容基本摘抄自《深入理解Java虛擬機(jī)》,以供復(fù)習(xí)之用,沒有多少參考價(jià)值。想要更詳細(xì)了解請參考原書。 第二章 1.運(yùn)行時(shí)數(shù)據(jù)區(qū)域 showImg(https://segment...
摘要:語言通過字節(jié)碼的方式,在一定程度上解決了傳統(tǒng)解釋型語言執(zhí)行效率低的問題,同時(shí)又保留了解釋型語言可移植的特點(diǎn)。有針對不同系統(tǒng)的特定實(shí)現(xiàn),,,目的是使用相同的字節(jié)碼,它們都會給出相同的結(jié)果。 showImg(https://segmentfault.com/img/bVbsjCK?w=800&h=450); 一、面向?qū)ο蠛兔嫦蜻^程的區(qū)別 面向過程優(yōu)點(diǎn): 性能比面向?qū)ο蟾撸驗(yàn)轭愓{(diào)用時(shí)需要實(shí)...
摘要:這個(gè)龐大的結(jié)構(gòu)主要包含以下幾個(gè)部分魔數(shù)和版本號基本的信息用于確定二進(jìn)制字節(jié)碼的特征和加載可行特征。 這篇文章的素材來自周志明的《深入理解Java虛擬機(jī)》。 作為Java開發(fā)人員,一定程度了解JVM虛擬機(jī)的的運(yùn)作方式非常重要,本文就一些簡單的虛擬機(jī)的相關(guān)概念和運(yùn)作機(jī)制展開我自己的學(xué)習(xí)過程,是這個(gè)系列的第二篇。 我們在文件里寫入了java的源代碼,源代碼寫就后存入磁盤,磁盤上的源代碼經(jīng)過j...
閱讀 2035·2023-04-25 14:50
閱讀 2914·2021-11-17 09:33
閱讀 2618·2019-08-30 13:07
閱讀 2845·2019-08-29 16:57
閱讀 913·2019-08-29 15:26
閱讀 3555·2019-08-29 13:08
閱讀 1996·2019-08-29 12:32
閱讀 3391·2019-08-26 13:57