JVM—如何利用虚拟机栈进行函数调用?
原文作者:老馬說(shuō)編程?
原文地址:Java編程的邏輯 (12) - 函數(shù)調(diào)用的基本原理
目錄
棧
變量的生命周期
數(shù)組和對(duì)象
遞歸調(diào)用
函數(shù)調(diào)用的成本
小結(jié)
讀后有收獲可以支付寶請(qǐng)作者喝奶茶?
棧
我們之前談過(guò)程序執(zhí)行的基本原理:CPU有一個(gè)指令指示器,指向下一條要執(zhí)行的指令,要么順序執(zhí)行,要么進(jìn)行跳轉(zhuǎn)(條件跳轉(zhuǎn)或無(wú)條件跳轉(zhuǎn))。基本上,這依然是成立的,程序從main函數(shù)開始順序執(zhí)行,函數(shù)調(diào)用可以看做是一個(gè)無(wú)條件跳轉(zhuǎn),跳轉(zhuǎn)到對(duì)應(yīng)函數(shù)的指令處開始執(zhí)行,碰到return語(yǔ)句或者函數(shù)結(jié)尾的時(shí)候,再執(zhí)行一次無(wú)條件跳轉(zhuǎn),跳轉(zhuǎn)回調(diào)用方,執(zhí)行調(diào)用函數(shù)后的下一條指令。但這里面有幾個(gè)問題:
- 參數(shù)如何傳遞?
- 函數(shù)如何知道返回到什么地方?在if/else, for中,跳轉(zhuǎn)的地址都是確定的,但函數(shù)自己并不知道會(huì)被誰(shuí)調(diào)用,而且可能會(huì)被很多地方調(diào)用,它并不能提前知道執(zhí)行結(jié)束后返回哪里。
- 函數(shù)結(jié)果如何傳給調(diào)用方?
解決思路是使用內(nèi)存來(lái)存放這些數(shù)據(jù),函數(shù)調(diào)用方和函數(shù)自己就如何存放和使用這些數(shù)據(jù)達(dá)成一個(gè)一致的協(xié)議或約定。這個(gè)約定在各種計(jì)算機(jī)系統(tǒng)中都是類似的,存放這些數(shù)據(jù)的內(nèi)存有一個(gè)相同的名字,叫棧。棧是一塊內(nèi)存,但它的使用有特別的約定,一般是先進(jìn)后出,類似于一個(gè)桶,往棧里放數(shù)據(jù),我們稱為入棧,最下面的我們稱為棧底,最上面的我們稱為棧頂,從棧頂拿出數(shù)據(jù),通常稱為出棧。
計(jì)算機(jī)系統(tǒng)主要使用棧來(lái)存放函數(shù)調(diào)用過(guò)程中需要的數(shù)據(jù),包括參數(shù)、返回地址,函數(shù)內(nèi)定義的局部變量也放在棧中。返回值不太一樣,它可能放在棧中,但它使用的棧和局部變量不完全一樣,有的系統(tǒng)使用CPU內(nèi)的一個(gè)存儲(chǔ)器存儲(chǔ)返回值,我們可以簡(jiǎn)單認(rèn)為存在一個(gè)專門的返回值存儲(chǔ)器。?main函數(shù)的相關(guān)數(shù)據(jù)放在棧的最下面,每調(diào)用一次函數(shù),都會(huì)將相關(guān)函數(shù)的數(shù)據(jù)入棧,調(diào)用結(jié)束會(huì)出棧。
以上描述可能有點(diǎn)抽象,我們通過(guò)一個(gè)例子來(lái)說(shuō)明。
一個(gè)簡(jiǎn)單的例子
我們從一個(gè)簡(jiǎn)單例子開始,下面是代碼:
?
1 public class Sum {2 3 public static int sum(int a, int b) {4 int c = a + b;5 return c;6 }7 8 public static void main(String[] args) {9 int d = Sum.sum(1, 2); 10 System.out.println(d); 11 } 12 13 }?
這是一個(gè)簡(jiǎn)單的例子,main函數(shù)調(diào)用了sum函數(shù),計(jì)算1和2的和,然后輸出計(jì)算結(jié)果,從概念上,這是容易理解的,讓我們從棧的角度來(lái)討論下。當(dāng)程序在main函數(shù)調(diào)用Sum.sum之前,棧的情況大概是這樣的:
主要存放了兩個(gè)變量args和d。在程序執(zhí)行到Sum.sum的函數(shù)內(nèi)部,準(zhǔn)備返回之前,即第5行,棧的情況大概是這樣的:
我們解釋下,在main函數(shù)調(diào)用Sum.sum時(shí),首先將參數(shù)1和2入棧,然后將返回地址(也就是調(diào)用函數(shù)結(jié)束后要執(zhí)行的指令地址)入棧,接著跳轉(zhuǎn)到sum 函數(shù),在sum函數(shù)內(nèi)部,需要為局部變量c分配一個(gè)空間,而參數(shù)變量a和b則直接對(duì)應(yīng)于入棧的數(shù)據(jù)1和2,在返回之前,返回值保存到了專門的返回值存儲(chǔ)器 中。
在調(diào)用return后,程序會(huì)跳轉(zhuǎn)到棧中保存的返回地址,即main的下一條指令地址,而sum函數(shù)相關(guān)的數(shù)據(jù)會(huì)出棧,從而又變回下面這樣:
main的下一條指令是根據(jù)函數(shù)返回值給變量d賦值,返回值從專門的返回值存儲(chǔ)器中獲得。函數(shù)執(zhí)行的基本原理,簡(jiǎn)單來(lái)說(shuō)就是這樣。但有一些需要介紹的點(diǎn),我們討論一下。
變量的生命周期
定義一個(gè)變量就會(huì)分配一塊內(nèi)存,但我們并沒有具體談什么時(shí)候分配內(nèi)存,具體分配在哪里,什么時(shí)候釋放內(nèi)存。從以上關(guān)于棧的描述我們可以看出,函數(shù)中的參數(shù)和函數(shù)內(nèi)定義的變量,都分配在棧中,這些變量只有在函數(shù)被調(diào)用的時(shí)候才分配,而且在調(diào)用結(jié)束后就被釋放了。但這個(gè)說(shuō)法主要針對(duì)基本數(shù)據(jù)類型,接下來(lái)我們談數(shù)組和對(duì)象。
數(shù)組和對(duì)象
對(duì)于數(shù)組和對(duì)象類型,我們介紹過(guò),它們都有兩塊內(nèi)存,一塊存放實(shí)際的內(nèi)容,一塊存放實(shí)際內(nèi)容的地址,實(shí)際的內(nèi)容空間一般不是分配在棧上的,而是分配在堆(也是內(nèi)存的一部分,后續(xù)文章介紹)中,但存放地址的空間是分配在棧上的。我們來(lái)看個(gè)例子,下面是代碼:
?
public class ArrayMax {public static int max(int min, int[] arr) {int max = min;for(int a : arr){if(a>max){max = a;}}return max;}public static void main(String[] args) {int[] arr = new int[]{2,3,4};int ret = max(0, arr);System.out.println(ret);}}?
這個(gè)程序也很簡(jiǎn)單,main函數(shù)新建了一個(gè)數(shù)組,然后調(diào)用函數(shù)max計(jì)算0和數(shù)組中元素的最大值,在程序執(zhí)行到max函數(shù)的return語(yǔ)句之前的時(shí)候,內(nèi)存中棧和堆的情況大概是這樣的:
對(duì)于數(shù)組arr,在棧中存放的是實(shí)際內(nèi)容的地址0x1000,存放地址的棧空間會(huì)隨著入棧分配,出棧釋放,但存放實(shí)際內(nèi)容的堆空間不受影響。但說(shuō)堆空間完全不受影響是不正確的,在這個(gè)例子中,當(dāng)main函數(shù)執(zhí)行結(jié)束,棧空間沒有變量指向它的時(shí)候,Java系統(tǒng)會(huì)自動(dòng)進(jìn)行垃圾回收,從而釋放這塊空間。
遞歸調(diào)用
我們?cè)偻ㄟ^(guò)棧的角度來(lái)理解一下遞歸函數(shù)的調(diào)用過(guò)程,代碼如下:
?
public static int factorial(int n) {if(n==0){return 1;}else{return n*factorial(n-1);} }public static void main(String[] args) {int ret = factorial(4);System.out.println(ret); }?
在factorial第一次被調(diào)用的時(shí)候,n是4,在執(zhí)行到 n*factorial(n-1),即4*factorial(3)之前的時(shí)候,棧的情況大概是:
注意返回值存儲(chǔ)器是沒有值的,在調(diào)用factorial(3)后,棧的情況變?yōu)榱?#xff1a;
棧的深度增加了,返回值存儲(chǔ)器依然為空,就這樣,每遞歸調(diào)用一次,棧的深度就增加一層,每次調(diào)用都會(huì)分配對(duì)應(yīng)的參數(shù)和局部變量,也都會(huì)保存調(diào)用的返回地址,在調(diào)用到n等于0的時(shí)候,棧的情況是:
這個(gè)時(shí)候,終于有返回值了,我們將factorial簡(jiǎn)寫為f。f(0)的返回值為1,f(0)返回到f(1),f(1)執(zhí)行1*f(0),結(jié)果也是1,然 后返回到f(2),f(2)執(zhí)行2*f(1),結(jié)果是2,然后接著返回到f(3),f(3)執(zhí)行3*f(2),結(jié)果是6,然后返回到f(4),執(zhí)行 4*f(3),結(jié)果是24。
以上就是遞歸函數(shù)的執(zhí)行過(guò)程,函數(shù)代碼雖然只有一份,但在執(zhí)行的過(guò)程中,每調(diào)用一次,就會(huì)有一次入棧,生成一份不同的參數(shù)、局部變量和返回地址。
函數(shù)調(diào)用的成本
從函數(shù)調(diào)用的過(guò)程我們可以看出,調(diào)用是有成本的,每一次調(diào)用都需要分配額外的棧空間用于存儲(chǔ)參數(shù)、局部變量以及返回地址,需要進(jìn)行額外的入棧和出棧操作。在遞歸調(diào)用的情況下,如果遞歸的次數(shù)比較多,這個(gè)成本是比較可觀的,所以,如果程序可以比較容易的改為別的方式,應(yīng)該考慮別的方式。另外,棧的空間不是無(wú)限的,一般正常調(diào)用都是沒有問題的,但像上節(jié)介紹的例子,棧空間過(guò)深,系統(tǒng)就會(huì)拋出錯(cuò)誤,java.lang.StackOverflowError,即棧溢出。
小結(jié)
本節(jié)介紹了函數(shù)調(diào)用的基本原理,函數(shù)調(diào)用主要是通過(guò)棧來(lái)存儲(chǔ)相關(guān)數(shù)據(jù)的,系統(tǒng)就函數(shù)調(diào)用者和函數(shù)如何使用棧做了約定,返回值我們簡(jiǎn)化認(rèn)為是通過(guò)一個(gè)專門的返回值存儲(chǔ)器存儲(chǔ)的,我們主要從概念上介紹了其基本原理,忽略了一些細(xì)節(jié)。
在本節(jié)中,我們假設(shè)函數(shù)的修飾符都是public static,如果不是static的,則會(huì)略有差別,后續(xù)文章會(huì)介紹。
我們談到,在Java中,函數(shù)必須放在類中,目前我們簡(jiǎn)化認(rèn)為類只是函數(shù)的容器,但類在Java中遠(yuǎn)不止有這個(gè)功能,它還承載了很多概念和思維方式,在接下來(lái)的幾節(jié)中,讓我們一起來(lái)探索類的世界。
讀后有收獲可以支付寶請(qǐng)作者喝奶茶?
總結(jié)
以上是生活随笔為你收集整理的JVM—如何利用虚拟机栈进行函数调用?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 设计模式—工厂模式之简单工厂模式
- 下一篇: JVM—方法区到底是怎么保存函数方法的?