日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

JVM方法调用说明

發布時間:2023/12/20 编程问答 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 JVM方法调用说明 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

原文鏈接:http://www.jianshu.com/p/56a7c4b26b14

前言

Java具備三種特性:封裝、繼承、多態。
Java文件在編譯過程中不會進行傳統編譯的連接步驟,方法調用的目標方法以符號引用的方式存儲在Class文件中,這種多態特性給Java帶來了更靈活的擴展能力,但也使得方法調用變得相對復雜,需要在類加載期間,甚至到運行期間才能確定目標方法的直接引用。

方法調用

所有方法調用的目標方法在Class文件里面都是常量池中的符號引用。在類加載的解析階段,如果一個方法在運行之前有確定的調用版本,且在運行期間不變,虛擬機會將其符號引用解析為直接調用。

這種 編譯期可知,運行期不可變 的方法,主要包括靜態方法和私有方法兩大類,前者與具體類直接關聯,后者在外部不可訪問,兩者都不能通過繼承或別的方式進行重寫。

JVM提供了如下方法調用字節碼指令:

  • invokestatic:調用靜態方法;
  • invokespecial:調用實例構造方法,私有方法和父類方法;
  • invokevirtual:調用虛方法;
  • invokeinterface:調用接口方法,在運行時再確定一個實現此接口的對象;
  • invokedynamic:在運行時動態解析出調用點限定符所引用的方法之后,調用該方法;
  • 通過invokestatic和invokespecial指令調用的方法,可以在解析階段確定唯一的調用版本,符合這種條件的有靜態方法、私有方法、實例構造器和父類方法4種,它們在類加載時會把符號引用解析為該方法的直接引用。

    invokestatic

    class StaticTest {public static void hello() {System.out.println("hello");}public static void main(String args[]) {hello();} }

    通過javap命令查看main方法字節碼

    可以發現hello方法是通過invokestatic指令調用的。

    invokespecial

    class VirtualTest {private int id;public static void main(String args[]) {new VirtualTest();} }

    通過javap命令查看main方法字節碼

    可以發現實例構造器是通過invokespecial指令調用的。

    通過invokestatic和invokespecial指令調用的方法,可以稱為非虛方法,其余情況稱為虛方法,不過有一個特例,即被final關鍵字修飾的方法,雖然使用invokevirtual指令調用,由于它無法被覆蓋重寫,所以也是一種非虛方法。

    非虛方法的調用是一個靜態的過程,由于目標方法只有一個確定的版本,所以在類加載的解析階段就可以把符合引用解析為直接引用,而虛方法的調用是一個分派的過程,有靜態也有動態,可分為靜態單分派、靜態多分派、動態單分派和動態多分派。

    靜態分派

    靜態分派發生在代碼的編譯階段。

    public class StaticDispatch {static abstract class Humnan {}static class Man extends Humnan {}static class Woman extends Humnan {}public void hello(Humnan guy) {System.out.println("hello, Humnan");}public void hello(Man guy) {System.out.println("hello, Man");}public void hello(Woman guy) {System.out.println("hello, Woman");}public static void main(String[] args) {Humnan man = new Man();Humnan woman = new Woman();StaticDispatch dispatch = new StaticDispatch();dispatch.hello(man);dispatch.hello(woman);} }

    運行結果:

    hello, Humnan hello, Humnan

    相信有經驗的同學看完代碼后就能得出正確的結果,但為什么會這樣呢?先看看main方法的字節碼指令

    通過字節碼指令,可以發現兩次hello方法都是通過invokevirtual指令進行調用,而且調用的是參數為Human類型的hello方法。

    Humnan man = new Man();

    上述代碼中,變量man擁有兩個類型,一個靜態類型Human,一個實際類型Man,靜態類型在編譯期間可知。
    在編譯階段,Java編譯器會根據參數的靜態類型決定調用哪個重載版本,但在有些情況下,重載的版本不是唯一的,這樣只能選擇一個“更加合適的版本”進行調用,所以不建議在實際項目中使用這種模糊的方法重載。

    動態分派

    在運行期間根據參數的實際類型確定方法執行版本的過程稱為動態分派,動態分派和多態性中的重寫(override)有著緊密的聯系。

    public class DynamicDispatch {static abstract class Humnan {abstract void say();}static class Man extends Humnan {@Overridevoid say() {System.out.println("hello, i'm Man");}}static class Woman extends Humnan {@Overridevoid say() {System.out.println("hello, i'm Woman");}}public static void main(String[] args) {Humnan man = new Man();Humnan woman = new Woman();man.say();woman.say();} }

    運行結果:

    hello, i'm Man hello, i'm Woman

    對于習慣了面向對象思維的同學對于這個結果應該是理所當然的。這種情況下,顯然不能再根據靜態類型來決定方法的調用了,導致不同輸出結果的原因很簡單,man和woman的實際類型不同,但是JVM如何根據實際類型決定需要調用哪個方法?

    main方法的字節碼指令

    1.字節碼0 ~ 15行對應以下代碼:

    Humnan man = new Man(); Humnan woman = new Woman();

    在Java堆上申請內存空間和實例化對象,并將這兩個實例的引用分別存放到局部變量表的第1、2位置的Slot中。

    2.字節碼16~21行對應以下代碼:

    man.say(); woman.say();

    16和20行指令分別把之前存放到局部變量表1、2位置的對象引用壓入操作數棧的棧頂,這兩個對象是執行say方法的接收者(Receiver),17和21行指令進行方法調用。

    可以發現,17和21兩條指令完全一樣,但最終執行的目標方法卻不相同,這得從invokevirtual指令的多態查找說起了,invokevirtual指令在運行時分為以下幾個步驟:

  • 找到操作數棧的棧頂元素所指向的對象的實際類型,記為C;
  • 如果C中存在描述符和簡單名稱都相符的方法,則進行訪問權限驗證,如果驗證通過,則直接返回這個方法的直接引用,否則返回java.lang.IllegalAccessError異常;
  • 如果C中不存在對應的方法,則按照繼承關系對C的各個父類進行第2步的操作;
  • 如果各個父類也沒對應的方法,則返回異常;
  • 所以上述兩次invokevirtual指令將相同的符號引用解析成了不同對象的直接引用,這個過程就是Java語言中重寫的本質。

    JVM動態分派實現

    由于動態分派是非常頻繁的動作,因此在虛擬機的實際實現中,會基于性能的考慮,并不會如此頻繁的搜索對應方法,一般會在方法區中建立一個虛方法表,使用虛方法表代替方法查詢以提高性能。

    虛方法表在類加載的連接階段進行初始化,存放著各個方法的實際入口地址,如果某個方法在子類中沒有被重寫,那么子類的虛方法表中該方法的入口地址和父類保持一致。

    abstract class Humnan {abstract void say();void run() {System.out.println("Human is run");} } class Man extends Humnan {@Overridevoid say() {System.out.println("hello, i'm Man");}@Overridevoid run() {System.out.println("Man is run");} } class Woman extends Humnan {@Overridevoid say() {System.out.println("hello, i'm Humnan");} }

    對應的虛方法表結構

    由于在Woman類中沒有重寫run方法,因此在Woman的虛方法表中,run方法直接指向Human實例。

    總結

    以上是生活随笔為你收集整理的JVM方法调用说明的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。