1. 程式人生 > >再談方法呼叫與堆疊

再談方法呼叫與堆疊

再談方法呼叫與堆和棧

在JVM裡面,最重要的兩個執行時資料區,無非就是堆和棧了。

關於堆

堆記憶體是被多個執行緒共享的,而棧記憶體是執行緒私有的。堆主要用來儲存執行時所有的物件資料和各種陣列,簡單點說通過new建立的例項,都會在堆上分配空間。堆在虛擬機器啟動時建立,並且堆具有自動垃圾回收的功能,在Java的世界裡,程式設計師是沒辦法直接銷燬你所建立的物件的,一切必須由GC垃圾回收器來完成,也就是你用完後的物件,並不是立即銷燬的,而是在下一次gc發生時來完成回收的,堆的記憶體可以是固定的,也可以動態增長,並且不要求在記憶體裡面是必須連續的,如果計算需要更多的記憶體,超過了當前有效的記憶體,那麼就會丟擲OutOfMemoryError異常。

堆裡面還分配了一部分記憶體用於:

(1)方法區:

主要用來儲存我們編譯後的程式碼,包括每個類的結構,欄位,方法資料,常量池等,如果記憶體不足也會發生OutOfMemoryError異常。

(2)執行時常量池

這個其實是方法區裡面劃分的一個區域,主要用來儲存每個類或者接口裡面的常量池表,包括我們熟悉的字串常量池等。如果記憶體不足也會發生OutOfMemoryError異常

(3)本地的方法棧

為了支援native方法而存在的一部分割槽域,本地方法棧與虛擬機器棧一樣,也是執行緒私有的,發生的異常包括StackOverflowError和OutOfMemoryError。

關於棧

棧主要分虛擬機器棧和本地方法棧,我們這裡僅僅關注虛擬機器棧。

我們先來看下Oracle文件的官網解釋:

Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at the same time as the thread. A Java Virtual Machine stack stores frames (§2.6). A Java Virtual Machine stack is analogous to the stack of a conventional language such as C: it holds local variables and partial results, and plays a part in method invocation and return. Because the Java Virtual Machine stack is never manipulated directly except to push and pop frames, frames may be heap allocated. The memory for a Java Virtual Machine stack does not need to be contiguous.

簡單的說,棧屬於執行緒私有的,每一個執行緒都有一個自己的棧,棧裡面可以儲存資料,這個待會細說。此外還負責方法的呼叫和返回,java的棧僅僅負責 壓棧和出棧,棧記憶體本身是可以從堆上分配出來的,並且棧記憶體可以是不連續的。如果執行緒計算需要一個更大的棧超過了允許的值,就會丟擲StackOverflowError異常,如果棧記憶體還允許動態增加,那麼當下一次申請的記憶體,不滿足當前的需要,就會丟擲OutOfMemoryError異常。

前面說過棧可以儲存資料,這其實是在棧幀(frame)裡面完成的,主要儲存local變數,也執行動態連結,給方法返回值,還負責分發異常。棧幀與方法與一對一的關係,也就是說,每次虛擬機器呼叫一個方法時,就會生成一個frame,無論是否發生異常,當方法呼叫完成後總是銷燬,正在執行的方法,其frame稱為當前棧幀,當前棧幀執行完成會後,就會拋棄,然後繼續呼叫下一個方法的棧幀,此時該棧幀就會變成當前棧幀,直到所有的棧幀執行完畢,程式才執行結束。對一個類的一個方法,在呼叫時對應一個棧幀,棧幀包含三部分內容:

(1)方法本身的local變數陣列

單個local變數的值型別,包括boolean, byte, char, short, int, float, reference, 和 returnAddress,兩個local變數可以儲存long和double型別的值,注意這些都是定長型別,也就是說在方法裡面宣告上面提到的型別,其儲存可以直接在棧上,但同樣的型別如果是成員變數,那麼儲存就在堆上,這一點需要注意,另外棧上儲存的是定長,像字串(底層是char陣列),各種物件例項,資料本身都是儲存在堆上,棧裡面僅僅儲存 指標,也叫記憶體地址。

(2)方法裡面的操作符棧

每個棧幀裡面還包含一個後進先出的操作符棧(operand stack),這個主要是進行一些算術運算操作的,比如遇到的加減乘除等操作符等。

(3)當前方法執行時常量池的的引用

這裡面主要是一些執行時常量池的引用,用於支援方法程式碼的動態連結。動態連結主要轉變符號連結為真實的連結。

說了這麼多,我們總結一下棧的特點:

首先是執行緒私有的,不同的執行緒擁有不同的棧,棧裡面的資料,相互之前是不可見的。棧裡面可以直接儲存基本型別的資料,此外包括指標的記憶體地址,及方法的返回值,這些資料的記憶體分配都是在棧上,這也是我們為什麼說方法裡面的local變數是執行緒安全的原因,因為是執行緒私有,不涉及多執行緒的問題。棧裡面包含了很多幀,在程式執行時的每個方法,都會生成一個幀入棧,執行的過程就是出棧的過程。如果棧裡面引用了成員變數或者其他共享的變數,這個時候需要注意執行緒安全問題,因為這些變數是儲存在堆上的。

最後我們來看下,堆和棧的圖示:

一個分析的例子

下面,我們通過一個例子,來簡單看下,方法在棧裡面是如何執行的:

public class StackCallDemo {


    static class Cat{
        public String name;
    }

    public void m1(){
        int x=20;
        m2(x);// call m2 method
    }


    public void m2(int x){
        boolean c;
        m3();//call m3 method
    }


    public void m3(){
        Cat cat=new Cat();
        //more code
    }


    public static void main(String[] args) {


        StackCallDemo stackDemo=new StackCallDemo();
        stackDemo.m1();

    }


}


這個類程式碼非常簡單,方法執行邏輯 main=> m1=> m2=> m3,注意這是呼叫順序,也是入棧順序,出棧順序,也就是真正的執行順序,剛好相反,圖示如下:

注意每個出棧執行完的方法,就相當於銷燬了,在堆裡面的Cat物件,如果方法不再引用,那麼就再次gc時,會被回收掉。通過上圖,我們可以清晰的看到巢狀方法執行過程,想清楚這一點,我們再去理解遞迴方法就容易多了,如果你按照巢狀的方式,去思考遞迴,那肯定理解不了,但是我們按照棧的邏輯,去理解遞迴,就會發現容易多了,這裡沒有巢狀,只有順序入棧,出棧,分別對應遞和歸。

總結:

本文主要介紹了Java裡面堆和棧在執行時的資料區域和功能,並在文末結合了一個例子來演示了Java程式方法是如何執行的,瞭解方法的執行邏輯,有助於我們理解其工作原理,從而可以讓我們更好的去分析一些複雜的方法邏輯或者演算法,比如遞迴等。