1. 程式人生 > >深入理解JVM_java代碼的執行機制01

深入理解JVM_java代碼的執行機制01

功能 存在 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:存放了下一條要執行的指令在方法內的編譯量。 棧:存放了棧幀,每個方法每次調用都會產生棧幀。 棧幀分為:局部變量和操作數棧。 作用: 局部變量:存放方法中的局部變量和參數。 操作數棧:存放方法執行過程中的中間結果。
局部變量區 操作數棧
Stack Frame(棧幀)
局部變量區 操作數棧
Stack Frame(棧幀)
pc寄存器 Stack
三種執行方式: (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中執行。

深入理解JVM_java代碼的執行機制01