天天看點

InsideJVM(5)-Java Stack(堆棧)

Java堆棧

jvm為每個新建立的線程都配置設定一個堆棧。堆棧以幀為機關儲存

線程的狀态。jvm對堆棧隻進行兩種操作:以幀為機關的壓棧和出棧

操作。

某個線程正在執行的方法稱為此線程的目前方法。目前方法使用的幀稱

為目前幀。目前方法所屬的類稱為目前類。目前類的常量池稱為目前

常量池。當線程執行一個方法時,它會跟蹤目前的類和常量池。當jvm

會在目前幀内執行幀内資料的操作。

當線程激活一個java方法,jvm就會線上程的java堆棧裡新壓入一個幀。

這個幀自然成為了目前幀。在此方法執行期間,這個幀将用來儲存參數,

局部變量,中間計算過程和其他資料。

一個方法可以以兩種方法結束。一種是正常傳回結束。一種是通過

異常抛出而異常結束(abrupt completion)。不管以那種方式傳回,jvm

都會将目前幀彈出堆棧然後釋放掉,這樣上一個方法的幀就成為目前幀了。

(譯者:可能可以這樣了解,位于堆棧頂部的幀為目前幀)

java堆棧上的所有資料都為此線程私有。一個線程不能通路另一個線程

的堆棧資料,是以在多線程的情況下也不需要對堆棧資料的通路進行同步。

象方法區和堆一樣(見以前的譯文),java堆棧和幀在記憶體中也不必是連續

的。幀可以分布在連續的記憶體區,也可以不是。幀的資料結構由jvm的實作者

來決定,他們可以允許使用者指定java堆棧的初始大小或最大最小尺寸。

堆棧幀( The Stack Frame)

堆棧幀有三部分:局部變量區,操作數堆棧和幀資料區。局部變量區和操作數堆棧

的大小要視對應的方法而定。編譯器在編譯的時候就對每個方法進行了計算并放在

了類檔案(class file)中了。幀資料區的大小對一種jvm實作來說是一定的。

當jvm激活一個方法時,它從類資訊資料得到此方法的局部變量區和操作數堆棧的

大小,并據此配置設定大小合适堆棧幀壓入java堆棧中。

局部變量區

java堆棧幀的局部變量區是一個基為零類型為word的數組。指令通過索引來

使用這些資料。類型為int,float,reference和returnAddress的值在

數組中占據一項,類型為byte,short,和char的值在存入數組前都轉為了

int值而占據一項。類型為long和double的值在數組中占據連續的兩項,在

通路他們的時候,指令提供第一項的索引。例如一個long值占據3,4項,指令會

取索引為3的long值。局部變量區的所有值都是字對齊的,long和doubles

的起始索引值沒有限定。

局部變量區包含此方法的參數和局部變量。編譯器首先以聲明的順序把參數

放入局部資料區。圖5-9顯示了下面兩個方法的變量區。

// On CD-ROM in file jvm/ex3/Example3a.java

class Example3a {

    public static int runClassMethod(int i, long l, float f,

        double d, Object o, byte b) {

        return 0;

    }

    public int runInstanceMethod(char c, double d, short s,

        boolean b) {

        return 0;

    }

}

InsideJVM(5)-Java Stack(堆棧)

圖5-9. 局部變量區中的方法參數

注意在方法runInstanceMethod()的幀中,第一個參數是一個

類型為reference的值,盡管方法沒有顯示的聲明這個參數,但

這是個對每個執行個體方法(instance method)都隐含加入的一個

參數值,用來代表調用的對象。(譯者:與c++中的this指針一樣)

我們看方法runClassMethod()就沒有這個變量,這是因為這是一

個類方法(class method),類方法與類相關,而不與對象相關。

我們注意到在源碼中的byte,short,char和boolean在局部變量區

都成了ints。在操作數堆棧也是同樣的情況。如前所述,jvm不直接

支援boolean類型,java編譯器總是用ints來表示boolean。但java

對byte,short和char是支援的,這些類型的值可以作為執行個體變量

存儲在局部變量區中,也可以作為類變量存儲在方法區中。但在局部變量區

和操作數堆棧中都被轉成了ints類型的值,期間的運算也是以int來的,

隻當存回堆或方法區中,才會轉回原來的類型。

同樣需要注意的是runClassMethod()的對象o。在java中,是以的對象

都以引用(reference)傳遞。所有的對象都存儲在堆中,你永遠都不會在

局部變量區或操作數堆棧中發現對象的拷貝,隻會有對象引用。

編譯器對局部變量的放置方法可以多種多樣,它可以任意決定放置順序,

甚至可以用一個索引指代兩個局部變量。例如,當兩個局部變量的作用域

不重疊時,如Example3b的局部變量i和j。

// On CD-ROM in file jvm/ex3/Example3b.java

class Example3b {

    public static void runtwoLoops() {

        for (int i = 0; i < 10; ++i) {

            System.out.println(i);

        }

        for (int j = 9; j >= 0; --j) {

            System.out.println(j);

        }

    }

}

jvm的實作者對局部變量區的設計仍然有象其他資料區一樣的靈活性。

關于long和double資料如何分布在數組中,jvm規範沒有指定。

假如一個jvm實作的字長為64位,可以把long或double資料放在

數組中的低項内,而使高項為空。(在字長為32位的時候,需要兩項

才能放下一個long或double)。

操作數堆棧

操作數堆棧象局部變量區一樣是用一個類型為word的數組存儲資料,

但它不是通過索引來通路的,而是以堆棧的方式壓入和彈出。假如

一個指令壓入了一個值,另一個指令就可以彈出這個值并使用之。

jvm在操作數堆棧中的處理資料類型的方式和局部變量區是一樣的,同樣

有資料類型的轉換。jvm沒有寄存器,jvm是基于堆棧的而不是基于寄存器

的,因為jvm的指令從堆棧中獲得操作數,而不是寄存器。雖然操作數還可以

從另外一些地方獲得,如位元組碼中,或常量池内,但主要是從堆棧獲得的。

jvm把操作數堆棧當作一個工作區使用。許多指令從此堆棧中彈出資料,進行

運算,然後壓入結果。例如,iadd指令從堆棧中彈出兩個數,相加,然後壓入

結果。下面顯示了jvm是如何進行這項操作的:

iload_0    // push the int in local variable 0

iload_1    // push the int in local variable 1

iadd       // pop two ints, add them, push result

istore_2   // pop int, store into local variable 2

在這個位元組碼的序列裡,前兩個指令iload_0和iload_1将存儲在

局部變量區中索引為0和1的整數壓入操作資料區中,然後相加,将

結果壓入操作資料區中。第四條指令istore_2從操作資料區中彈出

結果并存儲到局部資料區索引為2的地方。在圖5-10中,詳細的表述

了這個過程,圖中,沒有使用的區域以空白表示。

InsideJVM(5)-Java Stack(堆棧)

圖5-10. 兩個局部變量的相加.

幀資料區

除了局部變量區和操作資料堆棧外,java棧幀還需要資料來支援

常量池解析(constant pool resolution),方法的正常傳回

(normal method return)和異常分派(exception dispatch)。

這些資訊儲存在幀資料區中。

jvm中的許多指令都涉及到常量池的資料。一些指令僅僅是取出常量池

中的資料并壓入操作數堆棧中。一些指令使用常量池中的資料來訓示

需要執行個體化的類或數組,需要通路的域,或需要激活的方法。還有一些

指令來判斷某個對象是否是常量池指定的某個類或接口的子孫執行個體。

每當jvm要執行需要常量區資料的指令,它都會通過幀資料區中指向

常量區的指針來通路常量區。以前講過,常量區中對類型,域和方法

的引用在開始時都是符号。如果當指令執行的時候仍然是符号,jvm

就會進行解析。

除了常量區解析外,幀資料區還要幫助jvm處理方法的正常和異常結束。

正常結束,jvm必須恢複方法調用者的環境,包括恢複pc指針。假如

方法有傳回值,jvm必須将值壓入調用者的操作數堆棧。

為了處理方法的異常退出,幀資料區必須儲存對此方法異常表的引用。

一個異常表定義了這個方法受catch子句保護的區域,每項都有一個

catch子句的起始和開始位置(position),和用來表示異常類在常量池

中的索引,以及catch子句代碼的起始位置。

當一個方法抛出異常時,jvm使用幀數組區指定的異常表來決定如何處理。

如果找到了比對的catch子句,就會轉交控制權。如果沒有發現,方法會

立即結束。jvm使用幀資料區的資訊恢複調用者的幀,然後重新抛出同樣

的異常。

除了上述資訊外,jvm的實作者也可以将其他資訊放入幀資料區,如調試

資料。

java堆棧的一種實作

實作者可以按自己的想法設計java堆棧。如以前所講,一個方法是從堆中

單獨的配置設定幀。我以此為例,看下面的類:

// On CD-ROM in file jvm/ex3/Example3c.java

class Example3c {

    public static void addAndPrint() {

        double result = addTwoTypes(1, 88.88);

        System.out.println(result);

    }

    public static double addTwoTypes(int i, double d) {

        return i + d;

    }

}

圖5-11顯示了一個線程執行這個方法的三個快照。在這個jvm的實作中,

每個幀都單獨的從堆中配置設定。為了激活方法addTwoTypes(),方法

addAndPrint()首先壓入int 1和double88.88到操作數堆棧中,然後

激活addTwoTypes()方法。

InsideJVM(5)-Java Stack(堆棧)

圖5-11. 幀的配置設定

激活addTwoTypes()的指令使用了常量池的資料,jvm在常量池中查找這些資料

如果有必要則解析之。

注意addAndPrint()方法使用常量池引用方法addTwoTypes(),盡管

這兩個方法是屬于一個類的。象引用其他類一樣,對同一個類的方法和域

的引用在初始的時候也是符号,在使用之前需要解析。

解析後的常量池資料項将指向存儲在方法區中有關方法addTwoTypes()的資訊。

jvm将使用這些資訊決定方法addTwoTypes()局部變量區和操作數堆棧的大小。

如果使用Sun的javac編譯器(JDK1.1)的話,方法addTwoTypes()的局部變量區

需要三個words,操作數堆棧需要四個words。(幀資料區的大小對某個jvm實作

來說是定的)jvm為這個方法配置設定了足夠大小的一個堆棧幀。然後從方法

addAndPrint()的操作數堆棧中彈出double參數和int參數(88.88和 1)并把他們

分别放在了方法addTwoType()的局部變量區索引為1和0的地方。

當addTwoTypes()傳回時,它首先把類型為double的傳回值(這裡是89.88)

壓入自己的操作數堆棧裡。jvm使用幀資料區中的資訊找到調用者(為

addAndPrint())的堆棧幀,然後将傳回值壓入addAndPrint()的操作數堆棧

中并釋放方法addTwoType()的堆棧幀。然後jvm使addTwoType()的堆棧幀

為目前幀并繼續執行方法addAndPrint()。

圖5-12顯示了相同的方法在不同的jvm實作裡的執行情況。這裡的堆棧幀是在

一個連續的空間裡的。這種方法允許相鄰方法的堆棧幀可以重疊。這裡調用者的

操作數堆棧就成了被調者的局部變量區。

InsideJVM(5)-Java Stack(堆棧)

圖5-12. 從一個連續的堆棧中配置設定幀

這種方法不僅節省了空間,而且節省了時間,因為jvm不必把參數從一個

堆棧幀拷貝到另一個堆棧幀中了。

注意目前幀的操作數堆棧總是在java堆棧的頂部。盡管這樣可能

可以更好的說明圖5-12的實作。但不管java堆棧是如何實作的,

對操作數堆棧的操作總是在目前幀執行的。這樣,在目前幀的

操作數堆棧壓入一個數也就是在java堆棧壓入一個值。

java堆棧還有一些其他的實作,基本上是上述兩種的結合。一個jvm可以

線上程初期時從堆棧分出一段空間。在這段連續的空間裡,jvm可以采用

5-12的重疊方法。但在與其他段空間的結合上,就要使用如圖5-11的方法。