偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

虛擬機(jī)字節(jié)碼執(zhí)行引擎

云計(jì)算 虛擬化
所謂的「虛擬機(jī)字節(jié)碼執(zhí)行引擎」其實(shí)就是 JVM 根據(jù) Class 文件中給出的字節(jié)碼指令,基于棧解釋器的一種執(zhí)行機(jī)制。通俗點(diǎn)來(lái)說(shuō),也就是 JVM 解析字節(jié)碼指令,輸出運(yùn)行結(jié)果的一個(gè)過(guò)程。接下來(lái)我們?cè)敿?xì)看看這部分內(nèi)容。

所謂的「虛擬機(jī)字節(jié)碼執(zhí)行引擎」其實(shí)就是 JVM 根據(jù) Class 文件中給出的字節(jié)碼指令,基于棧解釋器的一種執(zhí)行機(jī)制。通俗點(diǎn)來(lái)說(shuō),也就是 JVM 解析字節(jié)碼指令,輸出運(yùn)行結(jié)果的一個(gè)過(guò)程。接下來(lái)我們?cè)敿?xì)看看這部分內(nèi)容。

[[224954]]

方法調(diào)用的本質(zhì)

在描述「字節(jié)碼執(zhí)行引擎」之前,我們先從匯編層面看看基于棧幀的方法調(diào)用是怎樣的。(以 IA32 型 CPU 指令集為例)

IA32 的程序中使用棧幀數(shù)據(jù)結(jié)構(gòu)來(lái)支持過(guò)程調(diào)用(Java 語(yǔ)言中稱作方法),每個(gè)過(guò)程對(duì)應(yīng)一個(gè)棧幀,過(guò)程的調(diào)用對(duì)應(yīng)與棧幀的入棧和出棧。某個(gè)時(shí)刻,只有位于棧頂?shù)臈捎茫砹四硞€(gè)方法正在執(zhí)行中的各種狀態(tài)。最頂端的棧幀用兩個(gè)指針界定,棧指針,幀指針。他們對(duì)應(yīng)于棧中的地址分別存儲(chǔ)在寄存器 %ebp 和 %esp 中。棧中的大致結(jié)構(gòu)如下:

棧指針始終指向棧頂元素,控制著棧中元素的出入棧,幀指針指向的是當(dāng)前棧幀的底部,注意是當(dāng)前棧幀,不是整個(gè)棧的底部。

下面我們看看一段 C 代碼:

  1. #include<stdio.h> 
  2. void sayHello(int age) 
  3.     int x = 32; 
  4.     int y = 2323; 
  5.     age = x + y; 
  6.  
  7. void main() 
  8.     int age = 22; 
  9.     sayHello(age); 

很簡(jiǎn)單的一段代碼,我們匯編生成相應(yīng)的匯編代碼,省略了部分鏈接代碼,留下的是核心的部分:

  1. main: 
  2.     pushl   %ebp 
  3.     movl    %esp, %ebp 
  4.     subl            $20, %esp 
  5.     movl    $22, -4(%ebp) 
  6.     movl    -4(%ebp), %eax 
  7.     movl    %eax, (%esp) 
  8.     call            sayHello 
  9.     leave 
  10.     ret 
  11.      
  12. sayHello: 
  13.     pushl   %ebp 
  14.     movl    %esp, %ebp 
  15.     subl            $16, %esp 
  16.     movl    $32, -4(%ebp) 
  17.     movl    $2323, -8(%ebp) 
  18.     movl    -8(%ebp), %eax 
  19.     movl    -4(%ebp), %edx 
  20.     addl            %edx, %eax 
  21.     movl    %eax, -12(%ebp) 
  22.     leave 
  23.     ret 

先看 main 函數(shù)的匯編代碼,main 函數(shù)里的前兩個(gè)匯編指令和 sayHello 中的前兩條指令是一樣的,我們?cè)诹舻胶笳呃锝榻B。

subl 指令將寄存器 %esp 中的地址減去 20,即棧指針向上擴(kuò)展了 20 個(gè)字節(jié)(棧是倒著生長(zhǎng)的),也就是為當(dāng)前棧幀分配了 20 個(gè)字節(jié)大小。接著,movl 將值 20 寫入地址 -4(%ebp),這個(gè)地址其實(shí)就是相對(duì)寄存器 %ebp 幀指針位置之上的四個(gè)字節(jié)處。假如 %ebp 的值為:0x14,那么 20 就被存儲(chǔ)到地址 0x10 的棧地址中。

接著一條 movl 指令將參數(shù) age 的值取出來(lái)存入寄存器 %eax 中。

這時(shí)就到了核心的 call 方法了,計(jì)算機(jī)中有程序計(jì)數(shù)器(PC)來(lái)指向下一條指令的位置,而常常我們的程序會(huì)調(diào)用到其他方法里,那么調(diào)用結(jié)束后又該如何恢復(fù)調(diào)用前的狀態(tài)并繼續(xù)執(zhí)行呢?

這里的解決辦法是,call 指令的***步就是將返回地址壓棧,然后跳向 sayHell 方法中執(zhí)行,這里我們看不到它壓棧的過(guò)程,被集成為一條指令了。

然后跳向了 sayHello 方法的***條指令開(kāi)始執(zhí)行,pushl 將寄存器 %ebp 中的地址壓棧,這時(shí)候的 %ebp 是上一個(gè)棧幀的幀指針地址,這個(gè)操作其實(shí)是一個(gè)保存的動(dòng)作。然后,movl 指令將幀指針指向棧指針的位置,也就是棧頂位置,繼而將棧指針向上擴(kuò)展 16 個(gè)字節(jié)。

接著,將數(shù)值 32 和 2323 分別寫入不同的棧地址中,這個(gè)地址相對(duì)于幀指針的地址,是可以計(jì)算出來(lái)的。

后面的操作是將 x 和 y 分別寫入寄存器 %eax 和 %edx,然后 add 指令做加法運(yùn)算并存入寄存器 %eax 中。接著將結(jié)果壓棧。

leave 指令等效于以下兩條指令之和:

  1. movl %ebp %esp 
  2. popl %ebp 

什么意思呢?

把棧指針退回到幀指針的位置,也就是當(dāng)前棧幀的底部,接著彈棧,這樣的話整個(gè) sayHello 所占用的棧幀就已經(jīng)無(wú)法引用了,相當(dāng)于釋放了當(dāng)前棧幀。

ret 指令用于恢復(fù)調(diào)用前的狀態(tài),繼續(xù)執(zhí)行 main 方法。

整個(gè) IA32 的方法調(diào)用基本如上,對(duì)于 64 位的 x86-64 來(lái)說(shuō),增加了 16 個(gè)寄存器,優(yōu)先使用寄存器進(jìn)行參數(shù)的計(jì)算與傳遞,效率提高了。但是與這個(gè)基于棧的存儲(chǔ)方式來(lái)說(shuō),劣勢(shì)之處在于「可移植性差」,不同的機(jī)器的寄存器使用肯定是有所差別的。所以我們的 Java 毋庸置疑使用的是棧。

運(yùn)行時(shí)棧幀結(jié)構(gòu)

在 Java 中,一個(gè)棧幀對(duì)應(yīng)一個(gè)方法調(diào)用,方法中需涉及到的局部變量、操作數(shù),返回地址等都存放在棧幀中的。每個(gè)方法對(duì)應(yīng)的棧幀大小在編譯后基本已經(jīng)確定了,方法中需要多大的局部變量表,多深的操作數(shù)棧等信息早以被寫入方法的 Code 屬性中了。所以運(yùn)行期,方法的棧幀大小早已固定,直接計(jì)算并分配內(nèi)存即可。

局部變量表

局部變量表用來(lái)存放方法運(yùn)行時(shí)用到的各種變量,以及方法參數(shù)。虛擬機(jī)規(guī)范中指明,局部變量表的容量用變量槽(slot)為最小單位,卻沒(méi)有指明一個(gè) slot 的實(shí)際空間大小,只是說(shuō),每個(gè) slot 應(yīng)當(dāng)能夠存放任意一個(gè) boolean,byte,char,short,int,float,reference 等。

按照我的理解,一個(gè) slot 相當(dāng)于一個(gè)黑盒子,具體占幾個(gè)字節(jié)適情況而定,但是這個(gè)黑盒子明確可以保存一個(gè)任意類型的變量。

局部變量表不同于操作數(shù)棧,它采用索引機(jī)制訪問(wèn)元素,而不同于操作數(shù)棧的出入棧方式。例如:

  1. public void sayHello(String name){ 
  2.         int x = 23; 
  3.         int y = 43; 
  4.         x++; 
  5.         x = y - 2; 
  6.         long z = 234; 
  7.         x = (int)z; 
  8.         String str = new String("hello wrold "); 
  9.     } 

我們反編譯看看它的局部變量表:

可以看到,局部變量表***項(xiàng)是名為 this 的一個(gè)類引用,它指向堆中當(dāng)前對(duì)象的引用。接著就是我們的方法參數(shù),局部變量 x,y,z 和 str。

這其實(shí)也間接說(shuō)明了,我們的每個(gè)實(shí)例方法都默認(rèn)傳入了一個(gè)參數(shù) this,指向當(dāng)前類的實(shí)例引用。

操作數(shù)棧

操作數(shù)棧也稱作操作棧,它不像局部變量表采用的索引機(jī)制訪問(wèn)其中元素,而是標(biāo)準(zhǔn)的棧操作,入棧出棧,先入后出。操作數(shù)棧在方法執(zhí)行之初為空,隨著方法的一步一步運(yùn)行,操作數(shù)棧中將不停的發(fā)生入棧出棧操作,直至方法執(zhí)行結(jié)束。

操作數(shù)棧是方法執(zhí)行過(guò)程中很重要的一個(gè)部分,方法執(zhí)行過(guò)程中各個(gè)中間結(jié)果都需要借助操作數(shù)棧進(jìn)行存儲(chǔ)。

返回地址

一個(gè)方法在調(diào)用另一個(gè)方法結(jié)束之后,需要返回調(diào)用處繼續(xù)執(zhí)行后續(xù)的方法體。那么調(diào)用其他方法的位置點(diǎn)就叫做「返回地址」,我們需要通過(guò)一定的手段保證,CPU 執(zhí)行其他方法之后還能返回原來(lái)調(diào)用處,進(jìn)而繼續(xù)調(diào)用者的方法體。

正如我們一開(kāi)始介紹的匯編代碼一樣,這個(gè)返回地址往往會(huì)被提前壓入調(diào)用者的棧幀中,當(dāng)方法調(diào)用結(jié)束時(shí),取出棧頂元素即可得到后續(xù)方法體執(zhí)行入口。

方法調(diào)用

方法調(diào)用算是本篇的一個(gè)核心內(nèi)容了,它解決了虛擬機(jī)對(duì)目標(biāo)調(diào)用方法的確定問(wèn)題,因?yàn)橥粭l虛擬機(jī)指令要求調(diào)用某個(gè)方法,但是該方法可能會(huì)有重載,重寫等問(wèn)題,那么虛擬機(jī)又該如何確定調(diào)用哪個(gè)方法呢?這就是本階段要處理的唯一任務(wù)。

首先我們要談?wù)勥@個(gè)解析過(guò)程,從上篇文章中可以知道,當(dāng)一個(gè)類初次加載的時(shí)候,會(huì)在解析階段完成常量池中符號(hào)引用到直接引用的替換。這其中就包括方法的符號(hào)引用翻譯到直接引用的過(guò)程,但這只針對(duì)部分方法,有些方法只有在運(yùn)行時(shí)才能確定的,就不會(huì)被解析。我們稱在類加載階段的解析過(guò)程為「靜態(tài)解析」。

那么哪些方法是被靜態(tài)解析了,哪些方法需要?jiǎng)討B(tài)解析呢?

比如下面這段代碼:

  1. Object obj = new String("hello");  
  2. obj.equals("world"); 

Object 類中有一個(gè) equals 方法,String 類中也有一個(gè) equals 方法,上述程序顯然調(diào)用的是 String 的 equals 方法。那么如果我們加載 Object 類的時(shí)候?qū)?equals 符號(hào)引用直接指向了本身的 equals 方法的直接引用,那么上述的 obj 永遠(yuǎn)調(diào)用的都是 Object 的 equals 方法。那我們的多態(tài)就永遠(yuǎn)實(shí)現(xiàn)不了。

只有那些,「編譯期可知,運(yùn)行時(shí)不變」的方法才可以在類加載的時(shí)候?qū)⑵溥M(jìn)行靜態(tài)解析,這些方法主要有:private 修飾的私有方法,類靜態(tài)方法,類實(shí)例構(gòu)造器,父類方法。

其余的所有方法統(tǒng)稱為「虛方法」,類加載的解析階段不會(huì)被解析。這些方法的調(diào)用不存在問(wèn)題,虛擬機(jī)直接根據(jù)直接引用即可找到方法的入口,但是「非虛方法」就不同了,虛擬機(jī)需要用一定的策略才能定位到實(shí)際的方法,下面我們一起來(lái)看看。

靜態(tài)分派

首先我們看一段代碼:

  1. public class Father { 
  2. public class Son extends Father { 
  3. public class Daughter extends Father { 
  4.  
  5.  
  6. public class Hello { 
  7.     public void sayHello(Father father){ 
  8.         System.out.println("hello , i am the father"); 
  9.     } 
  10.     public void sayHello(Daughter daughter){ 
  11.         System.out.println("hello i am the daughter"); 
  12.     } 
  13.     public void sayHello(Son son){ 
  14.         System.out.println("hello i am the son"); 
  15.     } 
  16.  
  17.  
  18. public static void main(String[] args){ 
  19.     Father son = new Son(); 
  20.     Father daughter = new Daughter(); 
  21.     Hello hello = new Hello(); 
  22.     hello.sayHello(son); 
  23.     hello.sayHello(daughter); 

輸出結(jié)果如下:

  1. hello , i am the father  
  2. hello , i am the father 

不知道你答對(duì)了沒(méi)有?這是一道很常見(jiàn)的面試題,考的就是你對(duì)方法重載的理解以及方法分派邏輯懂不懂。下面我們來(lái)分析一下:

首先需要介紹兩個(gè)概念,「靜態(tài)類型」和「實(shí)際類型」。靜態(tài)類型指的是包裝在一個(gè)變量最外層的類型,例如上述 Father 就是所謂的靜態(tài)類型,而 Son 或是 Daughter 則是實(shí)際類型。

我們的編譯器在生成字節(jié)碼指令的時(shí)候會(huì)根據(jù)變量的靜態(tài)類型選擇調(diào)用合適的方法。就我們上述的例子而言:

這兩個(gè)方法就是我們 main 函數(shù)中調(diào)用的兩次 sayHello 方法,但是你會(huì)發(fā)現(xiàn)傳入的參數(shù)類型是相同的,F(xiàn)ather,也就是調(diào)用的方法是相同的,都是這個(gè)方法:

  1. (LStaticDispathch/Father;)V 

也就是

  1. public void sayHello(Father father){} 

所有依賴靜態(tài)類型來(lái)定位方法執(zhí)行版本的分派動(dòng)作稱作「靜態(tài)分派」,而方法重載是靜態(tài)分派的一個(gè)典型體現(xiàn)。但需要注意的是,靜態(tài)分派不管你實(shí)際類型是什么,它只根據(jù)你的靜態(tài)類型進(jìn)行方法調(diào)用。

動(dòng)態(tài)分派

  1. public class Father { 
  2.     public void sayHello(){ 
  3.         System.out.println("hello world ---- father"); 
  4.     } 
  5. public class Son extends Father { 
  6.     @Override 
  7.     public void sayHello(){ 
  8.         System.out.println("hello world ---- son"); 
  9.     } 
  10.  
  11.  
  12. public static void main(String[] args){ 
  13.     Father son = new Son(); 
  14.     son.sayHello(); 

輸出結(jié)果:

  1. hello world ---- son 

顯然,最終調(diào)用了子類的 sayHello 方法,我們看生成的字節(jié)碼指令調(diào)用情況:

看到?jīng)]?編譯器為我們生成的方法調(diào)用指令,選擇調(diào)用的是靜態(tài)類型的對(duì)應(yīng)方法,但是為什么最終的結(jié)果卻調(diào)用了是實(shí)際類型的對(duì)應(yīng)方法呢?

當(dāng)我們將要調(diào)用某個(gè)類型實(shí)例的具體方法時(shí),會(huì)首先將當(dāng)前實(shí)例壓入操作數(shù)棧,然后我們的 invokevirtual 指令需要完成以下幾個(gè)步驟才能實(shí)現(xiàn)對(duì)一個(gè)方法的調(diào)用:

  • 彈出操作數(shù)棧頂部元素,判斷其實(shí)際類型,記做 C
  • 在類型 C 中查找需要調(diào)用方法的簡(jiǎn)單名稱和描述符相同的方法,如果有則返回該方法的直接引用
  • 否則,向 C 的父類再做搜索,有即返回方法的直接引用
  • 否則,拋出異常 java.lang.AbstractMethodError 異常

所以,我們此處的示例調(diào)用的是子類 Son 的 sayHello 方法就不言而喻了。

至于虛擬機(jī)為什么能這么準(zhǔn)確高效的搜索某個(gè)類中的指定方法,各個(gè)虛擬機(jī)的實(shí)現(xiàn)各有不同,但最常見(jiàn)的是使用「虛方法表」,這個(gè)概念也比較簡(jiǎn)單,就是為每個(gè)類型都維護(hù)一張方法表,該表中記錄了當(dāng)前類型的所有方法的描述信息。于是虛擬機(jī)檢索方法的時(shí)候,只需要從方法表中進(jìn)行搜索即可,當(dāng)前類型的方法表中沒(méi)有就去父類的方法表中進(jìn)行搜索。

動(dòng)態(tài)類型特性的支持

動(dòng)態(tài)類型語(yǔ)言的一個(gè)關(guān)鍵特征就是,類型檢查發(fā)生在運(yùn)行時(shí)。也就是說(shuō),編譯期間編譯器是不會(huì)管你這個(gè)變量是什么類型,調(diào)用的方法是否存在的。例如:

  1. Object obj = new String("hello-world"); 
  2. obj.split("-"); 

Java 中,兩行代碼是不能通過(guò)編譯器的,原因就是,編譯器檢查變量 obj 的靜態(tài)類型是 Object,而 Object 類中并沒(méi)有 subString 這個(gè)方法,故而報(bào)錯(cuò)。

而如果是動(dòng)態(tài)類型語(yǔ)言的話,這段代碼就是沒(méi)問(wèn)題的。

靜態(tài)語(yǔ)言會(huì)在編譯期檢查變量類型,并提供嚴(yán)格的檢查,而動(dòng)態(tài)語(yǔ)言在運(yùn)行期檢查變量實(shí)際類型,給了程序更大的靈活性。各有優(yōu)劣,靜態(tài)語(yǔ)言的優(yōu)勢(shì)在于安全,缺點(diǎn)在于缺乏靈活性,動(dòng)態(tài)語(yǔ)言則是相反的。

JDK1.7 提供了兩種方式來(lái)支持 Java 的動(dòng)態(tài)特性,invokedynamic 指令和 java.lang.invoke 包。這兩者的實(shí)現(xiàn)方式是類似的,我們只介紹后者的基本內(nèi)容。

  1. //該方法是我自定義的,并非 invoke 包中的 
  2. public static MethodHandle getSubStringMethod(Object obj) throws NoSuchMethodException, IllegalAccessException { 
  3.     //定義了一個(gè)方法模板,規(guī)定了待搜索的方法的返回值和參數(shù)類型 
  4.     MethodType methodType = MethodType.methodType(String[].class,String.class); 
  5.     //查找符合指定方法簡(jiǎn)單名稱和模板信息的方法 
  6.     return lookup().findVirtual(obj.getClass(),"split",methodType).bindTo(obj); 
  1. public static void main(String[] args){ 
  2.     Object obj = new String("hello-world"); 
  3.     //定位方法,并傳入?yún)?shù)執(zhí)行方法 
  4.     String[] strs = (String[]) getSubStringMethod(obj).invokeExact("-"); 
  5.     System.out.println(strs[0]); 

輸出結(jié)果:

  1. hello 

你看,雖然我們 obj 的靜態(tài)類型是 Object,但是通過(guò)這種方式,我就是能夠越過(guò)編譯器的類型檢查,直接在運(yùn)行期執(zhí)行我指定的方法。

具體如何實(shí)現(xiàn)的我就不帶大家看了,比較復(fù)雜,以后有機(jī)會(huì)單獨(dú)寫一篇文章學(xué)習(xí)一下。反正通過(guò)這種方式,我們可以不用管一個(gè)變量的靜態(tài)類型是什么,只要它有我想要調(diào)的方法,我們就可以在運(yùn)行期直接調(diào)用。

總結(jié)一下,HotSpot 虛擬機(jī)基于操作數(shù)棧進(jìn)行方法的解釋執(zhí)行,所有運(yùn)算的中間結(jié)果以及方法參數(shù)等等,基本都伴隨著出入棧的操作取出或存儲(chǔ)。這種機(jī)制***的優(yōu)勢(shì)在于,可移植性強(qiáng)。不同于基于寄存器的方法執(zhí)行機(jī)制,對(duì)底層硬件依賴過(guò)度,無(wú)法很輕易的跨平臺(tái),但是劣勢(shì)也很明顯,就是同樣的操作需要相對(duì)更多的指令才能完成。

 

責(zé)任編輯:武曉燕 來(lái)源: 撲在代碼上的高爾基
相關(guān)推薦

2024-10-20 13:28:47

虛擬機(jī)字節(jié)碼CPU

2023-01-09 18:30:53

架構(gòu)JVM

2010-02-24 10:39:28

Python虛擬機(jī)

2013-09-17 10:35:17

Python執(zhí)行原理

2021-05-28 23:04:23

Python利器執(zhí)行

2012-05-18 10:22:23

2010-09-25 15:13:40

JVMJava虛擬機(jī)

2013-07-17 09:32:58

2010-07-26 09:02:38

2023-06-02 14:18:55

2017-11-14 16:43:13

Java虛擬機(jī)線程

2022-08-30 07:00:18

執(zhí)行引擎Hotspot虛擬機(jī)

2019-03-19 15:30:42

程序員JVM虛擬機(jī)

2014-02-21 11:20:34

KVMXen虛擬機(jī)

2010-12-23 14:05:12

虛擬機(jī)

2023-09-03 17:05:20

虛擬機(jī)

2012-04-10 10:29:29

2020-01-17 10:52:37

無(wú)服務(wù)器容器技術(shù)

2011-11-30 14:12:05

JavaJVM虛擬機(jī)

2019-05-16 09:07:42

華為方舟編譯器
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)