深入理解JVM_java代碼的執行機制01
阿新 • • 發佈:2017-06-24
功能 存在 oot 對象實例 符號 token 類型 格式 找對象 本章學習重點:
1、Jvm:
如何將java代碼編譯為class文件。
如何裝載class文件及如何執行class文件。
jvm如何進行內存分配和回收。
jvm多線程:線程資源同步機制和線程之間交互的機制。
3.1 java代碼的執行機制
java源碼編譯機制。
1、三個步驟:
分析和輸入到符號表(Parse and Enter)
Parse過程所做的為詞法和語法分析。
詞法分析:將代碼字符串轉變為Token序列。
語法分析:根據語法由Token序列生成抽象語法樹。
Enter過程為將符合好輸入到符號表。
通常包括確定類的超類型和接口,根據需要添加默認構造器、將類中出現的符號輸入類自身的符號表中等。
註解處理(Annotation Processing)
主要用於處理用戶自定義的Annotation。
語義分析和生成class文件(Analyse and Generate)
Analyse基於抽象語法樹進行一系列的語義分析。
包括 將語法樹的名字、表達式等元素與變量、方法、類型等聯系到一起;
檢查變量使用前是否已聲明;
推導泛型方法的類型參數;
檢查類型匹配性;
檢查所有語句都可到達;
檢查所有checked exception都被捕獲或拋出;
檢查變量的確定性賦值(例如有返回值的方法必須確定有返回值);
檢查變量的確定性不重復賦值(例如聲明為final的變量等);
解除語法糖(消除if(false){...})形式的無用代碼;
將泛型java轉為普通java;
將含有語法糖的語法樹改為含有簡單語言結構的語法樹,(例如foreach循環、自動裝箱/拆箱等);
等。
完成上述步驟,開始生成class文件。步驟為:
(1)將實例成員初始化器收集到構造器中,將靜態成員初始化器收集為<clinit>();
(2)將抽象語法樹生成字節碼,采用的方法為後序遍歷語法樹,並進行最後的少量代碼轉換;
(3)從符號表生成class文件。
2、class文件包含了以下信息:
結構信息:
class文件格式版本號及各部分的數量與大小的信息。
元數據:
簡單來說,元數據對應的就是java源碼中“聲明”與“常量”的信息。
主要有:類/繼承的超類/實現的接口的聲明信息、域與方法聲明信息和常量池。
方法信息:
簡單來說,java源碼中“語句”與“表達式”對應的信息。
主要有:字節碼、異常處理器表、求值棧與局部變量區大小、求值棧的類型記錄、調試用符號信息。
類加載機制。
類加載機制是指class文件加載到JVM,並形成class對象的機制,之後應用就可對class對象進行實例化並調用。
1、分三個步驟:
裝載(load):
過程負責找到二進制字節碼並加載到JVM中。
鏈接(Link):
過程負責對二進制字節碼的格式進行校驗、初始化裝載類中的靜態變量及解析類中調用的接口、類。
校驗如果不符合,則拋出VerifyError;
校驗過程中如果碰到要引用到其他的接口和類,也會進行加載,如果失敗,則拋出NoClassDefFoundError。
JVM初始化類中的靜態變量,並賦默認值,最後對類中的所有屬性,方法進行驗證,如果該階段失敗,可能會造成NoSuchMethodError、NoSuchFieldError等錯誤信息。
初始化(init):
過程即執行類中的靜態初始化代碼,構造器代碼及靜態屬性的初始化,以下4種情況下初始化過程會被觸發執行:
調用了new;
反射調用了類中的方法;
子類調用了初始化;
JVM啟動過程中指定的初始化類。
JVM的類加載通過ClassLoader及其子類來完成,分為:
BootStrap Class Loader;
采用C++實現,並非 ClassLoader的子類。JDK啟動時會初始化此 ClassLoader;
Extension Class Loader;
用來加載擴展功能的一些jar包,例如:JDK目錄下有dns工具jar包等;
System Class Loader;
用來加載啟動參數中指定的Classpath中的jar包及目錄。
User-Defined Class Loader;
開發人員自行實現的ClassLoader。
2、類加載過程中的常見異常:
(1)ClassNotFoundException:
原因為在當前的ClassLoader中加載類時未找到類文件。
(2)NoClassDefFoundError:
原因為加載的類中引用到的另外的類不存在。
(3)LinkageError:
該異常在自定義ClassLoader的情況下更容易出現。原因是此類已經在ClassLoader加載過了,重復地加載會造成該異常。
(4)ClassCastException:
該異常有多種原因,JDK5支持泛型後,合理使用泛型可相對減少此異常的觸發。
類執行機制。
2種方式:
1、字節碼解釋執行方式
在源碼編譯階段將源碼編譯為JVM字節碼,是一種中間代碼的方式,要由JVM在運行期對其進行解釋並執行。
JVM采用四個指令來執行不同的方法調用:
(1)invokestatic:
對應調用static方法。
(2)invokevirtual:
對應調用對象實例的方法。
(3)invokeinterface:
對應調用接口。
(4)invokesprcial:
對應調用private方法和編譯源碼後生成的<init>方法——此方法為對象實例化時的初始化方法。
JDK基於棧的體系結構來執行字節碼:
線程在創建後,會產生程序計數器(PC registers)和棧(Stack)。
作用:
PC registers:存放了下一條要執行的指令在方法內的編譯量。
棧:存放了棧幀,每個方法每次調用都會產生棧幀。
棧幀分為:局部變量和操作數棧。
作用:
局部變量:存放方法中的局部變量和參數。
操作數棧:存放方法執行過程中的中間結果。
三種執行方式:
(1)指令解釋執行:
執行方式:獲取下一條指令,解碼並分派,然後執行。由於很多操作要將值放入到操作數棧中,導致了寄存器和內存要不斷地交換數據,效率不高。
SUN JDK進行優化,主要有:棧頂緩存和部分棧幀共享。
(2)棧頂緩存:
將本來位於操作數棧頂的值直接緩存到寄存器上,對於大部分只需要一個值的操作而言,無須將數據放入操作數棧,可直接在寄存器計算,然後放回操作數棧。
(3)部分棧幀共享:
當調用方法時,後一方法可將前一方法的操作數棧作為當前方法的局部變量,從而節省數據copy帶來的消耗。
2、編譯執行
為了解決解釋執行的效率問題,JDK提供將字節碼編譯為機器碼的支持,編譯在運行時進行,通常稱為JIT編譯器。
JDK在執行過程中對執行頻率高的代碼進行編譯,對執行不頻繁的代碼則繼續采用解釋的方式,所以JDK又稱為HotSpotVM。
在編譯上提供2種模式:client compiler和server compiler。
(1)client compiler:
C1 輕量級,只做少量性能開銷比高的優化,占用內存少,合適與桌面交互式應用。
在寄存器分配策略上,JDK6以後采用的為線性掃描寄存器分配算法。
其他地方優化:
方法內聯:
把調用到的方法的指令直接植入當前方法中。
去虛擬化:
裝載class之後,進行類層次分析,如類中的方法只提供一個實現類,那麽對於調用了此方法的代碼,也可進行方法內聯。
冗余消除:
指在編譯時,根據運行時狀況進行代碼的折疊和消除。
等。
(2)server compiler:
C2 重量級,大量的傳統編譯優化技巧來進行優化,占用內存多,適用於服務器端的應用。
采用的為傳統的圖著色寄存器分配算法。優化範圍更多的在於全局的優化。
收集的信息:分支的跳轉/不跳轉的頻率、某條指令上出現過的類型、是否出現過空值、是否出現過異常。
逃逸分析是很多優化的基礎。指的是根據運行狀態來判斷方法中的變量是否會被外部讀取,如不會則認為此變量是逃逸的。基於此在編譯時會做:
標量替換:
用標量替換聚合量。
好處:如果創建的對象並未用到其中的全部變量,則可以節省一定的內存。對於代碼執行,由於無須去找對象的引用,也會更快。
棧上分配:
如果沒有,會在棧上直接創建對象實例,而不是在JVM堆上。
好處:棧上分配更快。回收時隨方法的結束,對象也被回收了。
同步消除:
如果發現同步的對象未逃逸,就沒有同步的必要,在編譯時會直接去掉同步。
等。
運行後C1、C2編譯出來的機器碼如果不再符合優化條件,則會進行逆優化,也就是回到解釋執行的方式。
一種特殊的編譯為:OSR(On Stack Replace)。與C1、C2區別:
(1)OSR編譯只替換循環代碼體的入口;現象:方法的整段代碼被編譯了,但只有循環代碼體才執行編譯後的機器碼,其他部分仍然是解釋執行方式。
(2)C1、C2替換的方法調用的入口。
Sun JDK根據機器來選擇C1和C2模式。當CPU超過2核且內存大於2GB時默認為C2模式,但是32位windows機器始終為C1模式。
未選擇在啟動時即編譯成機器碼的原因:
(1)靜態編譯並不能根據程序的運行狀況來優化執行的代碼。
(2)解釋執行比編譯執行更節省內存。
(3)啟動時解釋執行的啟動速度比編譯再啟動更快。
未編譯期間解釋執行方式會比較慢,JDk主要依據方法上的2個計數器是否超過閥值。
(1)調用計數器,即方法被調用的次數。(CompileThreshold)
該值指當方法被調用多少次後,就編譯為機器碼。在client模式下默認為1500次,server模式下默認為10000次。
可通過啟動時添加-XX:CompileThreshold=10000來設置該值。
(2)回邊計數器,即方法中循環執行部分代碼的執行次數。(OnStackReplacePercentage)
該值用於計算是否觸發OSR編譯的閥值。默認情況下client模式為933,server模式為140。
該值通過啟動時添加-XX:OnStackReplacePercentage=140來設置。
註意:由於sun JDK這個特性,在對java代碼進行性能測試時,要尤其註意是否事先做了足夠次數的調用,以保證測試是公平的。
3、反射執行
反射和直接創建對象實例,調用方法的最大不同在於創建、方法調用的過程是動態的。
要實現動態調用,最直接的方法就是動態生成字節碼,並加載到JVM中執行。
局部變量區 | 操作數棧 | |
Stack Frame(棧幀) | ||
局部變量區 | 操作數棧 | |
Stack Frame(棧幀) | ||
pc寄存器 | Stack |
深入理解JVM_java代碼的執行機制01