還以這個圖為例,從.java到.class是編譯過程,從.class到機(jī)器碼是解釋過程。下面對其進(jìn)行分別優(yōu)化。在優(yōu)化過程中,對編譯階段的優(yōu)化主要是對前端編譯器的優(yōu)化,在運(yùn)行階段的優(yōu)化,主要是對即時編譯器的優(yōu)化。
編譯器優(yōu)化
編譯過程
以上為javac的編譯過程圖,以下為javac編譯過程的主體代碼。
下面對其步驟進(jìn)行詳細(xì)解讀
1、解析與填充符號表
詞法分析
將源代碼的字符流轉(zhuǎn)變?yōu)闃?biāo)記(Token)集合,標(biāo)記是編譯過程中的最小元素,如a,=,b,int。
語法分析
根據(jù)Token序列構(gòu)造抽象語法樹。以后編譯器基本不會再對源碼文件進(jìn)行操作了,后續(xù)的操作都是建立在抽象語法樹上。抽象語法樹是一種用來描述程序代碼語法結(jié)構(gòu)的樹形表示方式,節(jié)點(diǎn)代表代碼中的一個語法結(jié)構(gòu),例如修飾符,返回值等。
填充符號表
符號表是由一組符號地址和符號信息構(gòu)成的表格,用于編譯的不同階段。如在語義分析中,用于語義檢查和產(chǎn)生中間代碼;在目標(biāo)代碼生成階段,用于地址分配的依據(jù)。
2、注解處理器
這部分是插入式注解處理器在編譯期間對注解進(jìn)行處理的過程。其可以對語法樹進(jìn)行修改,一旦進(jìn)行了修改,編譯器將回到上面的第一步進(jìn)行重新處理,每一次循環(huán)稱為一個Round,也就是上圖中的回環(huán)過程。
3、語義分析與字節(jié)碼生成
在經(jīng)過語法分析后,生成的語法樹是一個結(jié)構(gòu)正確的源程序的抽象,但無法保證源程序是符合邏輯的。語義分析的任務(wù)是對結(jié)構(gòu)上正確的源程序進(jìn)行上下文有關(guān)性質(zhì)的審查。比如下面代碼中的錯誤只能在語義分析階段檢查出來。
boolean a=false; char b=2; int c=a+b
此階段包括如下4個步驟:
標(biāo)注檢查
變量使用前是否已被聲明、變量與賦值之間的數(shù)據(jù)類型是否能夠匹配等。還有一個常量折疊,即把a(bǔ)=1+2變?yōu)閍=3。所以在代碼中的a=1+2和a=3并不會增加程序運(yùn)行期cpu指令的運(yùn)算量。
數(shù)據(jù)及控制流分析
檢查程序局部變量在使用前是否有賦值,方法的每條路徑是否都有返回值,是否所有的受查異常都被正確處理了等問題。在類加載時也有一個數(shù)據(jù)及控制流分析,其目的基本是一致的,但校驗(yàn)的范圍不同,有些校驗(yàn)項(xiàng)只有在編譯期或運(yùn)行期才能運(yùn)行。
解語法糖
語法糖是在計算機(jī)語言中添加某種語法,其對語言的功能沒有影響,但是能提高程序的可讀性。語法糖包括泛型,自動拆裝箱等。虛擬機(jī)運(yùn)行時不支持這些語法,它們在編譯階段還原回基礎(chǔ)語法結(jié)構(gòu)。這個過程稱為解語法糖。
字節(jié)碼生成
將之前步驟生成的信息(語法樹、符號表)轉(zhuǎn)化成字節(jié)碼寫到磁盤中,然后添加和轉(zhuǎn)換了少量的代碼。如把字符串的加操作替換為StringBuffer或StringBuilder的append()操作。
至此,Class文件生成了。
語法糖
語法糖是java中添加某種語法,對語言的功能沒有影響,但是可以增加程序的可讀性。包括泛型、內(nèi)部類、枚舉類等。
1、泛型與類型擦除
泛型可用于類、接口和方法的創(chuàng)建中,用于對放入集合元素的類型的約束。泛型只在程序源碼中存在,在編譯階段有解語法糖的步驟,所以在.Class文件中,已經(jīng)變?yōu)榱嗽瓉淼脑愋土?。這個過程叫做類型擦除。
泛型擦除前:
public static void main(String[] args){ Map<String,String> map=new HashMap<>(); map.put("姓名","小明"); map.put("性別","男"); sout(map.get("姓名")); sout(map.get("性別")); }
泛型擦除后:
public static void main(String[] args){ Map map=new HashMap(); map.put("姓名","小明"); map.put("性別","男"); sout((String)map.get("姓名")); sout((String)map.get("性別")); }
所以ArrayList和ArrayList在運(yùn)行期時是同一個類。
2、自動拆裝箱、循環(huán)遍歷
這些是java中使用最多的語法糖。編譯前:
public static void main(String[] args){ List<Integer> list=Arrays.asList(1,2,3,4); int sum=0; for(int i:list){ sum +=i; } System.out.println(sum); }
編譯后:
public static void main(String[] args){ List list=Arrays.asList(new Integer[] { Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3), Integer.valueOf(4)}); int sum=0; for(Iterator localIterator=list.iterator();localIterator.hasNext();){ int i=((Integer)localIterator.next()).intValue(); sum +=i; } System.out.println(sum); }
可見,自動拆裝箱在編譯后被轉(zhuǎn)化為了對應(yīng)的包裝和還原方法,如Integer.valueOf()和Integer.intValue()。
遍歷循環(huán)則把代碼還原為了迭代器的實(shí)現(xiàn)。
3、條件編譯
根據(jù)布爾常量值的真假,編譯器會把分支中不成立的代碼塊消除掉。
public static void main(String[] args){ if(true){ sout("block 1"); }else{ sout("block 2"); } }
編譯后,代碼變?yōu)椋?/p>
public static void main(String[] args){ sout("block 1"); }
運(yùn)行期優(yōu)化
一般情況下,我們將.java編譯成.class,.class再解釋成機(jī)器碼。但是也有特殊的情況。有些代碼調(diào)用比較頻繁,比如某個方法或代碼塊的運(yùn)行特別頻繁,為了提高程序的執(zhí)行效率,在運(yùn)行時,虛擬機(jī)會把這個代碼直接編譯成機(jī)器碼,并進(jìn)行各種層次的優(yōu)化。這樣的代碼稱為熱點(diǎn)代碼。完成這個任務(wù)的編譯器被稱為即時編譯器。但是其并不是虛擬機(jī)必需的部分。
即時編譯器的概述
(1)為什么虛擬機(jī)要使用解釋器和編譯器并存的架構(gòu)?
虛擬機(jī)里包含著解釋器和編譯器。當(dāng)程序需要迅速啟動和執(zhí)行的時候,解釋器可以首先發(fā)揮作用,省去編譯的時間,立即執(zhí)行。在程序運(yùn)行后,隨著時間的推移,編譯器逐漸發(fā)揮作用,把越來越多的代碼編譯成本地代碼之后,可以獲取更高的執(zhí)行效率。
當(dāng)程序運(yùn)行環(huán)境中內(nèi)存資源限制較大,可以使用解釋執(zhí)行節(jié)約內(nèi)存,反之可以使用編譯來提升效率。
(2)為什么虛擬機(jī)要實(shí)現(xiàn)兩個不同的即時編譯器?
虛擬機(jī)中內(nèi)置了兩個即時編譯器,分別為Client Compiler和Server Compiler,又稱為C1和C2。
默認(rèn)只使用其中的一個,至于選擇哪個,取決于虛擬機(jī)會根據(jù)自身版本和宿主機(jī)器的硬件性能自動選擇運(yùn)行模式。用戶也可以使用“-client”、“-server”進(jìn)行指定。
(3)程序何時使用解釋器執(zhí)行?何時使用編譯器執(zhí)行?
虛擬機(jī)有一個分層編譯策略。
第0層:程序解釋執(zhí)行,解釋器不開啟性能監(jiān)控功能,可觸發(fā)第1層編譯
第1層:也稱為C1編譯,將字節(jié)碼編譯為本地代碼,進(jìn)行簡單、可靠的優(yōu)化,如有必要將加入性能監(jiān)控的邏輯。
第2層:也稱為C2編譯,將字節(jié)碼編譯為本地代碼,但是會啟用一些編譯耗時較長的優(yōu)化,甚至?xí)鶕?jù)性能監(jiān)控信息進(jìn)行一些不可靠的激進(jìn)優(yōu)化。
(4)哪些程序代碼會被編譯為本地代碼?如何編譯為本地代碼?
熱點(diǎn)代碼包括如下兩類,其均把整個方法作為編譯對象。
a、被多次調(diào)用的方法
b、被多次執(zhí)行的循環(huán)體
熱點(diǎn)探測是用來判斷一段代碼是否為熱點(diǎn)代碼,其方式有兩種:
a、基于采樣
b、基于計數(shù)器。HotSpot使用的是這種。它為每個方法準(zhǔn)備了兩類計數(shù)器:統(tǒng)計方法被調(diào)用次數(shù)的方法調(diào)用計數(shù)器和統(tǒng)計一個方法中循環(huán)體代碼執(zhí)行次數(shù)的回邊計數(shù)器。
(5)如何從外部觀察及時編譯器的編譯過程和編譯結(jié)果?
可以使用 -xx:+PrintCompilation 查看哪些方法被即時編譯器編譯了。
優(yōu)化技術(shù)有哪些?
虛擬機(jī)的即時編譯器在生成代碼時,采用了如下的代碼優(yōu)化技術(shù)。
(1)公共子表達(dá)式消除
如果一個表達(dá)式E已經(jīng)計算過了,那如果再次出現(xiàn)E時就不會再對它進(jìn)行計算。比如:
int d=(a*b)*12+c+(c+b*a)
如果這段代碼交給javac編譯器,則不會進(jìn)行任何優(yōu)化。如果交給即時編譯器,會被進(jìn)行如下步驟的優(yōu)化:
第一步:消除公共子表達(dá)式
int d=E*12+c+(c+E)
第二步:代數(shù)化簡:
int d=E*13+c*2
(2)數(shù)組邊界檢查消除
數(shù)組邊界檢查是什么?
如果有一個數(shù)組foo[],在java語言中訪問數(shù)組元素foo[i]的時候,系統(tǒng)將會自動進(jìn)行上下界的范圍檢查,檢查i是否滿足0≤i≤foo.length這個條件。
那怎么進(jìn)行消除呢?
a、把運(yùn)行期檢查提到編譯期完成。如foo[3],只要在編譯期根據(jù)數(shù)據(jù)流分析來確定foo.length的值,并判斷下標(biāo)“3”沒有越界,執(zhí)行的時候就不用判斷了。
b、隱式異常處理。這種思路通常用于空指針檢查和算符運(yùn)算中除數(shù)為零的情況。
if(foo!=null){ return foo.value; }else{ throw new NullPointException(); }
被隱式異常處理優(yōu)化后,變?yōu)槿缦麓a:
try{ return foo.value; }catch(segment_fault){ uncommon_trap(); }
除了數(shù)組邊界檢查消除,還有自動裝箱消除、安全點(diǎn)消除、消除反射等。
(3)方法內(nèi)聯(lián)
把目標(biāo)方法的代碼“復(fù)制”到發(fā)起調(diào)用的方法之中,避免發(fā)生真實(shí)的方法調(diào)用。
public int add(int x1, int x2, int x3, int x4) { return add1(x1, x2) + add1(x3, x4); } public int add1(int x1, int x2) { return x1 + x2; }
運(yùn)行一段時間后JVM會把a(bǔ)dd1方法去掉,并把代碼翻譯成:
public int add(int x1, int x2, int x3, int x4) { return x1 + x2 + x3 + x4; }
(4)逃逸分析
當(dāng)一個對象在方法中被定義后,它可能被外部方法所引用,比如作為調(diào)用參數(shù)傳遞到其它方法中,這稱為方法逃逸。同理,如果被外部線程訪問到,它就稱為線程逃逸。
對變量進(jìn)行相應(yīng)分析就叫做逃逸分析。如果能證明別的方法或線程無法通過任何途徑訪問到這個對象,則可以為這個變量進(jìn)行一些優(yōu)化。
優(yōu)化的手段有棧上分配、同步消除、標(biāo)量替換等。以同步消除為例,如果逃逸分析能夠確定一個變量不會逃逸出線程,即無法被其它線程訪問到,那對這個變量實(shí)施的同步措施就可以消除掉了。
以上內(nèi)容便是關(guān)于JAVA虛擬機(jī)中JVM優(yōu)化的全部介紹,