天天看點

學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程

hi 大家好,我是 DHL。公衆号:ByteCode ,專注分享有趣硬核原創内容,Kotlin、Jetpack、性能優化、系統源碼、算法及資料結構、動畫、大廠面經

全文分為 視訊版 和 文字版,

視訊版:

通過語音和動畫,能夠更加直覺的看到,記憶體記錄方法調用和傳回過程。

bilibili 位址: b23.tv/TQXL4xx

文字版

我們在寫代碼的時候有沒有思考過 方法如何調用 、 方法執行完之後如何傳回 、 記憶體如何記錄方法調用過程 。而這也是今天這篇文章重點内容。

方法調用和傳回過程涉及到了,虛拟機棧、程式計數器、局部變量表、操作數棧、方法傳回位址、動态連結等等内容,涉及到知識點很多,同時這些内容也是高頻面試題,是以我将拆分成多篇文章,針對每個知識點去做詳細的分析。而今天這篇文章我們重點去看記憶體是如何記錄方法調用和傳回過程。

虛拟機棧

Java 方法以棧幀的形式,運作在虛拟機棧(Java 棧)中,棧是線程私有的,程式啟動的時候,會建立一個 main 線程,作業系統會為每一個線程配置設定一段記憶體,線程建立的時候會建立一個虛拟機棧,虛拟機棧的生命周期和線程一樣,線程結束了,虛拟機棧也銷毀了。

每個 Java 方法,對應一個個棧幀,是以方法開始和結束,都是一個個棧幀入棧和出棧的過程,效果如下圖所示。

學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程

棧幀

每個 Java 方法,都是一個個棧幀,每個棧幀包括了:局部變量表、操作數棧、方法傳回位址、動态連結、附加資訊。

學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程
  • 局部變量表:儲存方法參數清單和方法内的局部變量,按照聲明的順序存儲,它以數組的形式展示,如果是執行個體方法,索引為 0 是 this 的引用,如下圖所示。
學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程
索引(Slot) 名字(Name)
this
1 num
2 res
  • 操作數棧:儲存方法執行過程中的臨時結果
  • 傳回位址:儲存調用該方法的 pc 寄存器的值(即 JVM 指令位址),用于方法結束時,傳回調用處,讓調用者方法繼續執行下去
  • 動态連結:指向運作時常量池中該棧幀所屬方法的引用,即從常量池中找到目标方法的符号引用,然後轉換為直接引用
  • 附加資訊:比如程式 debug 時添加的一些附件資訊(不重要,不需要關心,可忽略)

這裡隻需要知道它們的作用即可,它們的資料結構、位元組碼的含義、執行過程等等,後續的文章我将會針對每個知識點去做詳細的分析。

方法調用過程

先寫一段方法調用的代碼,首先會調用 ​

​main()​

​​ 方法之後調用 ​

​fun1()​

​​ 然後調用 ​

​fun2()​

​ ,如下圖所示。

學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程

現在我們來示範一下 Java 虛拟機執行這些 JVM 指令的過程,首先會調用 main () 方法。

main () 方法

​main()​

​ 方法執行流程動畫效果如下所示。

學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程
  1. 執行指令​

    ​0: aload_0​

    ​,從局部變量表中,讀取索引為 0 的值,壓入操作數棧中,因為是執行個體方法,是以索引為 0 的值是 this 的引用
學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程
  1. 執行指令​

    ​1: iconst_5 ​

    ​,将常量 5 壓入操作數棧中
學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程
  1. 執行指令​

    ​2: invokevirtual #7​

    ​​,常量 5 和 this 從操作數棧中出棧,然後調用​

    ​this.fun1(5)​

學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程

首先從常量池中找到方法 ​

​fun1()​

​​ 的符号引用,然後通過動态連結将符号引用轉換成直接引用,之後調用 ​

​this.fun1(5)​

​​,将方法 ​

​fun1()​

​​ 作為棧幀壓入虛拟機棧中,跳轉到 ​

​fun1()​

​,繼續往下執行。

如何從常量池中找到目标方法的符号引用,然後轉換成直接引用的過程,将會在後面系列文章中詳細分析

在調用 ​

​fun1()​

​​ 之前,​

​fun1()​

​ 的局部變量表和方法傳回位址已經确定好了。

學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程

進入方法 fun1 (int num)

方法 ​

​fun1(int num)​

​ 執行流程動畫效果如下所示。

學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程
  1. 執行指令​

    ​0: aload_0​

    ​,從局部變量表中,讀取索引為 0 的值,壓入操作數棧中。因為是執行個體方法,是以索引為 0 的值是 this 的引用
學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程
  1. 執行指令​

    ​1: iload_1​

    ​,從局部變量表中,讀取索引為 1 變量 num 的值,并壓入操作數棧
學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程
  1. 執行指令​

    ​2: invokevirtual #13​

    ​​,num 和 this 從操作數棧中出棧,然後調用​

    ​this.fun2(num)​

學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程

首先從常量池中找到方法 ​

​fun2()​

​​ 的符号引用,然後通過動态連結将符号引用轉換成直接引用,之後調用 ​

​this.fun2(num)​

​​,将方法 ​

​fun2()​

​​ 作為棧幀壓入虛拟機棧中,跳轉到 ​

​fun2()​

​​,繼續往下執行,調用 ​

​fun2()​

​​ 之前,​

​fun2()​

​ 的局部變量表和方法傳回位址已經确定好了。

學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程

進入方法 fun2 (int num)

方法 ​

​fun2(int num)​

​ 執行流程動畫效果如下所示。

學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程
  1. 執行指令​

    ​0: iload_1​

    ​,從局部變量表中,讀取索引為 1 變量 num 的值,并壓入操作數棧中
學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程
  1. 執行指令​

    ​1: bipush​

    ​ ,将常量 10 壓入操作數棧中
學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程
  1. 執行指令​

    ​3: iadd​

    ​ ,num 和常量 10 從操作數棧中出棧,然後進行相加,将結果壓入操作數棧中
學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程
  1. 執行指令​

    ​4: istore_2​

    ​,從操作數棧中取出結果,并指派給局部變量表中索引為 2 的變量 res2
學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程
  1. 執行指令​

    ​5: iload_2​

    ​,從局部變量表中,讀取索引為 2 的變量 res2 的值,并壓入操作數棧中
學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程

方法退出過程

每個方法即是一個棧幀,每個棧幀會儲存方法傳回位址,執行 ​

​return​

​ 系列指令時,會根據目前棧幀儲存的傳回位址,傳回到調用的位置,繼續往下執行。是以會分為兩種情況。

異常退出

如果出現了異常且捕獲了該異常,則會從異常表裡查找 PC 寄存器的位址(JVM 指令位址),傳回調用處繼續執行。

學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程

正常退出時會做以下件事

  • 恢複上一個棧幀局部變量表、操作數棧
  • 如果有傳回值,将傳回值壓入調用者棧幀的操作數棧,是否有傳回值根據​

    ​return​

    ​ JVM 指令:
  • ​ireturn​

    ​​ :傳回值是​

    ​boolean​

    ​​ 、​

    ​byte​

    ​​ 、​

    ​char​

    ​​ 、​

    ​short​

    ​​ 和​

    ​int​

    ​ 類型
  • ​lreturn​

    ​​ :傳回值是​

    ​Long​

    ​ 類型
  • ​freturn​

    ​​ :傳回值是​

    ​Float​

    ​ 類型
  • ​dreturn​

    ​​ :傳回值是​

    ​Double​

    ​ 類型
  • ​areturn​

    ​ :傳回值是引用類型
  • ​return​

    ​​ :傳回值類型為​

    ​void​

  • 設定調用者棧幀的 JVM 指令位址
  • 目前棧幀從虛拟機棧中出棧

這篇文章我們隻分析正常退出流程,異常退出和正常退出的流程大緻都是一樣的。正常退出流程動畫效果如下所示。

學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程
  1. 方法​

    ​fun2​

    ​​ 結束時,執行最後一條指令​

    ​ireturn​

    ​​ ,目前棧幀從虛拟機棧中出棧,根據目前棧幀儲存的方法傳回位址,傳回到 fun1 ,恢複 fun1 的局部變量表、操作數棧,将傳回結果​

    ​res2​

    ​ 儲存到調用者(fun1)操作數棧中
學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程
  1. 回到方法​

    ​fun1(int num)​

    ​​ ,執行指令​

    ​5: istore_2​

    ​​,變量​

    ​res2​

    ​​ 從操作數中出棧,指派給局部變量表中索引為 2 的變量​

    ​res1​

學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程
  1. 執行指令​

    ​6: iload_2​

    ​​ ,從局部變量表中,讀取索引為 2 變量​

    ​res1​

    ​ 的值,并壓入操作數棧中
學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程
  1. 執行方法 fun1 最後一條指令​

    ​7: ireturn​

    ​​,目前棧幀從虛拟機棧中出棧,根據目前棧幀儲存的方法傳回位址,傳回到 main 方法,恢複 main 方法的局部變量表、操作數棧,将傳回結果​

    ​res2​

    ​ 儲存到調用者(main)操作數棧中
學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程
  1. 最後回到方法​

    ​main​

    ​​ 中,執行最後一條指令​

    ​5: pop​

    ​,操作數棧中的元素出棧
學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程
  1. 執行最後一條指令​

    ​6: return​

    ​,至此所有的方法調用結束了,方法所占用的記憶體,也将傳回給系統
學習突破35歲焦慮,動畫示範記憶體記錄函數調用過程

每次的方法調用,即是棧幀入棧和出棧的過程,同時也需要占用部分記憶體,用于儲存局部變量表、操作數棧、方法傳回位址、動态連結、附加資訊等等。

當方法執行完,即棧幀出棧,方法調用所占用的記憶體,也将傳回給系統。

全文到這裡就結束了,感謝你的閱讀,如果有幫助,歡迎 ​

​在看​

​ 、 ​

​點贊​

​ 、 ​

​收藏​

​ 、 ​

​分享​

​ 給身邊的朋友。

真誠推薦你關注我,公衆号:ByteCode ,持續分享硬核原創内容,Kotlin、Jetpack、性能優化、系統源碼、算法及資料結構、動畫、大廠面經。

​近期必讀熱門文章

  • 揭秘反射真的很耗時嗎,射 10 萬次耗時多久
  • 你知道 Iterable 有多慢嗎?試試它提升性能
  • 揭秘 Kotlin 1.6.20 重磅功能 Context Receivers
  • Stack Overflow 上最熱門的 10 個 Kotlin 問題
  • Android 12 已來,你的 App 崩潰了嗎?
  • Google 宣布廢棄 LiveData.observe 方法
  • 影響性能的 Kotlin 代碼(一)
  • 揭秘 Kotlin 中的 == 和 ===
  • 個人部落格,将所有文章進行分類,歡迎前去檢視​​hi-dhl.com​​
  • KtKit 小巧而實用,用 Kotlin 語言編寫的工具庫,歡迎前去檢視​​KtKit​​
  • 計劃建立一個最全、最新的 AndroidX Jetpack 相關元件的實戰項目以及相關元件原理分析文章,正在逐漸增加 Jetpack 新成員,倉庫持續更新,歡迎前去檢視​​AndroidX-Jetpack-Practice​​
  • LeetCode / 劍指 offer / 國内外大廠面試題 / 多線程題解,語言 Java 和 kotlin,包含多種解法、解題思路、時間複雜度、空間複雜度分析
  • 劍指 offer 及國内外大廠面試題解:線上閱讀
  • LeetCode 系列題解:線上閱讀