java 执行机制_Java类的执行机制
在完成將class文件信息加載到JVM并產生Class對象后,就可執行Class對象的靜態方法或實例化對象進行調用了。在源碼編譯階段將源碼編譯為JVM字節碼,JVM字節碼是一種中間代碼的方式,要由JVM在運行期對其進行解釋并執行,這種方式成為字節碼解釋執行方式
字節碼解釋執行
由于采用的為中間碼的方式,JVM有一套自己的指令,對于面向對象的語言而言,最重要的是執行方法的指令,JVM采用了invokestatic、invokevirtual、invokeinterface、invokespecial四個指令來執行不同的方法調用。invokestatic對應的是調用static方法,invokevirtual對應的是調用對象實例的方法,invokeinterface對應的是調用接口的方法,invokespecial對應的是調用private方法和編譯源碼后生成的方法,此方法為對象實例化時的初始化方法(構造方法)。
Sun
JDK基于棧的體系結構來執行字節碼,基于棧方式的好處是代碼緊湊,體積小。線程在創建后,都會產生程序計數器(PC)和棧(Stack):PC存放了下一條要執行的指令在方法內的偏移量,棧中存放了棧幀,每個方法每次調用都會產生棧幀。棧幀主要分為局部變量區和操作數棧兩部分,局部變量區用于存放方法中的局部變量和參數,操作數棧中用于存放方法執行過程中產生的中間結果,棧幀中還會有一些雜用空間,例如指向方法已解析的常量池的引用、其他一些VM內部實現需要的數據等。
1、指令解釋執行
對于方法的指令解釋執行,執行方式為經典馮諾依曼體系中的FDX循環方法,即獲取下一條指令,解碼并分派,然后執行。在實現FDX循環式有switch-threading、token-threading、direct-threading、subroutine-threading、inline-threading等多種方式。Sun
JDK的重點為編譯成機器碼,并沒有在解釋器上做太復雜的處理,因此采用了token-threading方式。為了讓解釋執行能更加高效,Sun
JDK還做了一些其他的優化,主要有:棧頂緩存和部分棧幀共享。
2、棧頂緩存
在方法的執行過程中,可看到有很多操作要將值放入操作數棧,這導致了寄存器和內存要不斷的交換數據,Sun
JDK采用了一個棧頂緩存,即將本來位于操作數棧的值直接緩存在寄存器上,這對于大部分只需要一個值的操作而言,無需將數據放入操作數棧,可直接在寄存器計算,然后放回操作數棧。
3、部分棧幀共享
當一個方法調用另一個方法時,通常傳入另一個方法的參數為已存放在操作數棧的數據,Sun
JDK在此做了一個優化,就是當調用方法時,后一方法可將前一方法的操作數棧作為當前方法的局部變量,從而節省數據copy帶來的消耗。
編譯執行
編譯執行的效率較低,為提升代碼的執行性能,SunJDK提供將字節碼編譯為機器碼的支持,編譯在運行時進行,通常稱為JIT編譯器。SunJDK在執行過程中對執行頻率高的代碼進行編譯,對執行不頻繁的代碼則繼續采用解釋執行的方式,因此SunJDK又稱為Hotspot
VM,在編譯上SunJDK提供了兩種模式client compiler(-client)和server
compiler(-server)。
client
compliler又稱為C1,較為輕量級,只做少量性能開銷比高的優化,它占用內存較少,適合桌面交互式應用,在寄存器分配策略上,JDK6以后采用的為線性掃描寄存器分配算法,在其他方面的優化主要有:方法內嵌、去虛擬化、冗余消除等。
1、方法內聯
對于java面向對象的語言,通常要調用多個方法來完成功能,執行時,要經歷多次參數傳遞,返回值傳遞及跳轉等,于是C1采用了方法內聯的方式,即把調用到的方法的指令直接植入到當前的方法中。(可在debug版本的JDK的啟動參數加速-XX:PrintInlining來查看方法內聯的信息)
2、去虛擬化
去虛擬化是指在裝載class文件后,進行類層次的分析,如發現類中的方法只提供一個實現類,那么對于調用此方法的代碼,也可進行方法內聯,從而提升執行的性能。
3、冗余消除
冗余消除是指在編譯時,根據運行時狀況進行代碼折疊或消除。如某個判斷條件為false則可將條件內的代碼消除。
Server
Compiler又稱為C2,較為重量級,C2采用了大量傳統編譯優化技巧,占用內存會相對多,適用于服務器應用。和C1不同的主要是寄存器分配策略和優化的范圍,寄存器分配策略上C2采用的為傳統的圖著色寄存器分配算法:由于C2會收集程序的運行信息(收集的信息主要有,分支的跳轉/不跳轉的頻率,某條指令出新過的類型、是否出現過控制、是否出現過一場),因此其優化的范圍更多在于全局的優化,而不僅僅是一個方法塊的優化。
逃逸分析是C2進行很多優化的基礎,逃逸分析是指根據運行狀況來判斷方法中的變量是否會被外部讀取,如不會則認為此變量是逃逸的,基于逃逸分析C2在編譯時會做標量替換、棧上分配和同步消除等優化。
1、標量替換
變量替換的意思簡單來說就是用標量替換聚合量。這種方式能帶來的好處是,如果創建的對象并未用到其中的全部變量,則可以節省一定的內存。
2、棧上分配
如果變量沒有逃逸,那么C2會選擇在棧上直接創建對象實例,而不是在JVM堆上,在棧上分配的好處一方面是更加快速,另一方面是回收時隨著方法的結束,對象也被回收了。
3、同步消除
同步消除是指如果發現同步的對象未逃逸,那也沒有同步的必要了,在C2編譯時會直接去掉同步。
除了C1\C2外,還有一種較為特殊的編譯:OSR(On
Stack
Replace)。OSR和C1、C2的最主要的不同點在于OSR編譯只替換循環代碼體的入口,而C1、C2替換的是方法調用的入口,因此在OSR編譯后會出現的現象是方法的整段代碼都被編譯了,但在只有循環代碼體部分才執行編譯后的機器碼,其他部分仍然是是解釋執行方法。
默認情況下,SunJDK根據機器配置來選擇C1或C2模式,當機器配置CPU超過2核且內存超過2GB及默認為C2模式,但在32位機器上時鐘選擇的都是C1模式,也可在啟動時通過增加-client或-server來強制指定。
SunJDK為提升程序執行性能,在C1好C2上做了很多努力,其他各種實現的JVM也在編譯執行上做了很多的優化,SunJDK之所以未選擇在啟動時即編譯成機器碼,主要是因為:靜態編譯并不能根據程序的運行狀況來優化執行的代碼,C2這種方式是根據運行狀況來進行動態編譯的,如分支判斷、逃逸分析等,這些措施會提升程序執行的性能,在靜態編譯的情況下是無法實現的,給C2收集運行數據越長時間,編譯出的代碼也會越優;解釋執行比編譯執行更節省內存;啟動時解釋執行的啟動速度比編譯在啟動更快。
當程序在未編譯期間解釋執行會比較慢,因此需要取一個權衡值,在SunJDK中主要依據方法上的兩個計數器是否超過閾值,其中一個計數器為調用計數器,即方法被調用的次數;另一個為回邊計數器,即方法中循環執行部分代碼的執行次數。下面介紹兩個計數器對應的閾值
(1)ComplieThresold
該值是指方法被調用多少次后,就編譯為機器碼,在client模式下默認為1500次,在server模式下為10000次,可通過在啟動時天機-XX:CompilerThreshold=10000來設置該值
(2)OnStackReplacePercentage
該值用于計算是否觸發OSR編譯的閾值,默認模式下client為933,server模式下為140,該值可通過在啟動時添加-XX:OnStackReplacePercetage=140來設置。
反射執行
反射執行是Java的亮點之一,基于反射可動態調用某對象實例中對應的方法、訪問查看對象的屬性等,無需再編寫代碼時就確定要創建的對象。這使得Java可以很靈活的實現對象的調用,例如MVC框架中通常要調用實現類中的execute方法,但框架在編寫時是無法知道實現類的,在Java中則可以通過反射機制直接去調用應用類的實現類中的execute方法。
這種方式對于框架之類的代碼而言非常重要,反射和直接創建對象實例調用方法的最大不同在于創建的過程、方法調用的過程是動態的。這也是的采用反射生成執行方法調用的代碼并不像直接調用實例對象代碼,編譯后就可直接生成對象方法調用的字節碼,而是只能生成調用JVM反射實現的字節碼了。
要實現動態的調用,最直接的方法就是動態生成字節碼,并加載到JVM中執行,SunJDK采用的即為這種方法。
總結
以上是生活随笔為你收集整理的java 执行机制_Java类的执行机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 惠斯通电桥信号调理芯片_elmos推出专
- 下一篇: 哈希表数据结构_Java数据结构哈希表如