《自己動手寫Java虛擬機器》學習筆記(七)方法呼叫和返回
第七章 方法呼叫和返回
本章將實現方法呼叫和返回。還會討論類和物件的初始化。
7.1 方法呼叫概述
從呼叫的角度來看,方法可以分為兩類,靜態方法(或類方法)和例項方法。靜態方法通過類來呼叫,例項方法則通過物件引用來呼叫。靜態方法是靜態繫結的,也就是說,最終呼叫的是哪個方法在編譯期就已經確定;而例項方法支援動態繫結,最終要呼叫哪個方法需要到了執行期才知道。
從實現角度,可以分為:抽象方法,Java(或者JVM上的其他語言)實現和本地語言(如C/C++)實現。
Java7之前,有四條方法呼叫指令:invokestatic指令用來呼叫靜態方法。invokespecial指令用來呼叫無需動態繫結的例項方法,包括建構函式、私有方法和通過super關鍵字呼叫的超類方法。剩下的是動態繫結。如果針對介面型別的引用呼叫方法,用invokeinterface指令。否則用invokevirtual。
方法的呼叫需要1+N個運算元,其中第一個運算元是uint16索引,在位元組碼中緊跟在指令操作碼的後面。通過這個索引,可以從當前類執行池常量中找到一個方法符號引用,解析這個符號就可以得到一個方法。注意,這個方法並不一定就是最終要呼叫的那個方法,所以可能還需要一個查詢過程才能找到最終要呼叫的方法。剩下N個運算元是要傳遞給被呼叫方法的引數,從運算元棧中彈出。
如果要執行的是java方法,下一步給這個方法建立一個新的幀,並把它推導Java虛擬機器棧頂。
方法的最後一條指令是某個返回指令,這個指令負責把方法的返回值推入前一幀的運算元棧頂,然後把當前幀從java虛擬機器棧中彈出。
7.2 解析方法符號引用
對於非介面方法引用。如果類D想要通過方法符號引用訪問類C的某個方法,先要解析符號引用得到類C。如果C是介面,拋異常。否則根據方法名和描述符查詢方法,先從C的繼承層次中找,如果找不到,就去C的介面找,如果還是找不到拋異常。否則檢查類D是否有許可權訪問該方法,如果沒有,拋異常。
對於介面方法引用。與上面大同小異。
7.3 方法呼叫和引數傳遞
在定位到需要呼叫的方法之後,Java虛擬機器要給這個方法建立一個新的幀並把它推入Java虛擬機器棧頂,然後傳遞引數。
對於引數傳遞。首先,要確定方法的引數在區域性變量表中佔多少位置。注意,這個數量並不一定等於從Java程式碼總看到的引數個數,原因①long和double型別要佔用兩個位置②對於例項方法,java編譯器會在引數列表前面新增一個引數,這個隱藏引數就是this引用。
7.4 返回指令
return指令只需要把當前幀衝java虛擬機器棧中彈出即可。
其他返回指令需要從把方法的返回值推入前一幀的運算元棧頂,然後把當前幀從java虛擬機器棧中彈出。
7.5 方法呼叫指令
invokestataic指令。假定解析符號引用後得到方法M。M必須是靜態方法,否則丟擲異常。(M不能是類初始化方法!類初始化方法只能由Java虛擬機器呼叫,不能使用invokestatic呼叫!這一規則有class檔案驗證器保證)如果宣告M的類還沒有被初始化,則要先初始化該類。
invokespecial指令。①先拿到當前類、當前常量池、方法符號引用,然後解析符號引用、拿到解析後的類和方法。②假定從方法符號引用中解析出來的類是C,方法是M。如果M是建構函式,則宣告M的類必須是C,否則丟擲異常。如果是靜態方法,拋異常。③運算元棧彈出this引用,如果是null,拋異常(NullPointerException)。注意,在傳遞引數之前,不能破壞運算元棧的狀態。④確保protected方法只能被宣告該方法的類或子類呼叫,如果違反則拋異常。⑤如果呼叫超類中的函式但不是建構函式,且當前類的ACC_SPUER標誌被設定,需要一個額外查詢過程尋找最終要呼叫的方法,否則前面從方法符號引用解析出來的方法就是要呼叫的方法。⑥如果查詢失敗,或者找到的方法是抽象的,拋異常。如果找到就呼叫。
invokespecial指令。①②③與invokespecial大同小異。④從物件的類中查詢真正要呼叫的方法,如果找到的是一個抽象方法則拋異常,否則正常
invokeinterface指令。該指令操作碼後面跟著4位元組,而上述三條指令都是2位元組。前兩位元組含義和其他指令相同,是uint16型別的執行時常量池索引。第三位元組是給方法傳遞引數需要的slot數。第四位元組必須是0(給oracle的某些jvm使用)。①先從執行時常量池中拿到解析介面方法符號引用,如果解析方法是靜態方法或者私有方法拋異常。②從運算元棧彈出this引用,如果是null拋異常;如果引用所指物件的類沒有實現解析出來的介面,拋異常。③查詢最終呼叫的方法。如果找不到,或者找到的方法是抽象的,拋異常;如果找到的方法不是public,拋異常。否則正常。
7.6 改進直譯器
改進loop(迴圈):
每次迴圈開始,先拿到當前幀,然後根據pc從當前方法中解碼一條指令。指令執行完畢之後,判斷Java虛擬機器棧中是否還有幀。如果沒有則退出迴圈;否則繼續。
7.7 類初始化
類初始化就是執行類的初始化方法(<clinit>)。類的初始化方法發生在:
①執行new指令建立類例項,但類還有沒被初始化。
②執行putstatic、getstatic指令存取類的靜態變數,但宣告該欄位的類還有沒被初始化。
③執行invokestatic呼叫類的靜態方法,但是宣告該方法的類還有沒被初始化。
④當初始化一個類時,它的超類還沒有初始化,要先初始化超類。
⑤執行某些反射操作時。
因此,我們需要修改這四條指令的邏輯。先判斷類的初始化是否已經開始,如果還沒有,則需要呼叫類的初始化方法,並終止指令執行。但是由於PC已經執行了指向下一條指令,所以要修改PC的值使其重新指向當前指令。