Java字節碼淺析(—)


英文原文鏈接譯文鏈接,原文作者:James Bloom,譯者:有孚

明白Java代碼是如何編譯成字節碼並在JVM上運行的非常重要,這有助於理解程序運行的時候究竟發生了些什么。理解這點不僅能搞清語言特性是如何實現的,並且在做方案討論的時候能清楚相應的副作用及權衡利弊。

本文介紹了Java代碼是如何編譯成字節碼並在JVM上執行的。想了解JVM的內部結構以及字節碼運行時用到的各個內存區域,可以看下我前面的一篇關於JVM內部細節的文章

 

本文分為三部分,每一部分都分成幾個小節。每個小節都可以單獨閱讀,不過由於一些概念是逐步建立起來的,如果你依次閱讀完所有章節會更簡單一些。每一節都會覆蓋到Java代碼中的不同結構,並詳細介紹了它們是如何編譯並執行的。

1. 第一部分, 基礎概念

變量

局部變量

JVM是一個基於棧的架構。方法執行的時候(包括main方法),在棧上會分配一個新的幀,這個棧幀包含一組局部變量。這組局部變量包含了方法運行過程中用到的所有變量,包括this引用,所有的方法參數,以及其它局部定義的變量。對於類方法(也就是static方法)來說,方法參數是從第0個位置開始的,而對於實例方法來說,第0個位置上的變量是this指針。

局部變量可以是以下這些類型:

* char
* long
* short
* int
* float
* double
* 引用
* 返回地址

除了long和double類型外,每個變量都只占局部變量區中的一個變量槽(slot),而long及double會占用兩個連續的變量槽,因為這些類型是64位的。

當一個新的變量創建的時候,操作數棧(operand stack)會用來存儲這個新變量的值。然后這個變量會存儲到局部變量區中對應的位置上。如果這個變量不是基礎類型的話,本地變量槽上存的就只是一個引用。這個引用指向堆的里一個對象。

比如:

int i = 5;

編譯后就成了

0: bipush      5
2: istore_0
 bipush  用來將一個字節作為整型數字壓入操作數棧中,在這里5就會被壓入操作數棧上。
 istore_0 

這是istore_這組指令集(譯注:嚴格來說,這個應該叫做操作碼,opcode ,指令是指操作碼加上對應的操作數,oprand。

不過操作碼一般作為指令的助記符,這里統稱為指令)中的一條,這組指令是將一個整型數字存儲到本地變量中。

n代表的是局部變量區中的位置,並且只能是0,1,2,3。再多的話只能用另一條指令istore了,這條指令會接受一個操作數,對應的是局部變量區中的位置信息。

這條指令執行的時候,內存布局是這樣的:

class文件中的每一個方法都會包含一個局部變量表,如果這段代碼在一個方法里面的話,你會在類文件的局部變量表中發現如下的一條記錄。

LocalVariableTable:
        Start  Length  Slot  Name   Signature
          0      1      1     i         I
字段

Java類里面的字段是作為類對象實例的一部分,存儲在堆里面的(類變量對應存儲在類對象里面)。

關於字段的信息會添加到類文件里的field_info數組里,像下面這樣:

ClassFile {
    u4 magic;
    u2 minor_version;
    u2 major_version;
    u2 constant_pool_count;
    cp_info contant_pool[constant_pool_count – 1];
    u2 access_flags;
    u2 this_class;
    u2 super_class;
    u2 interfaces_count;
    u2 interfaces[interfaces_count];
    u2 fields_count;
    field_info fields[fields_count];
    u2 methods_count;
    method_info methods[methods_count];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

另外,如果變量被初始化了,那么初始化的字節碼會加到構造方法里。

下面這段代碼編譯了之后:

public class SimpleClass {

    public int simpleField = 100;

}

如果你用javap進行反編譯,這個被添加到了field_info數組里的字段會多出一段描述信息。

1 public int simpleField;
2     Signature: I
3     flags: ACC_PUBLIC

初始化變量的字節碼會被加到構造方法里,像下面這樣:

 1 public SimpleClass();
 2   Signature: ()V
 3   flags: ACC_PUBLIC
 4   Code:
 5     stack=2, locals=1, args_size=1
 6        0: aload_0
 7        1: invokespecial #1                  // Method java/lang/Object."<init>":()V
 8        4: aload_0
 9        5: bipush        100
10        7: putfield      #2                  // Field simpleField:I
11       10: return
aload_0

從局部變量數組中加載一個對象引用到操作數棧的棧頂。盡管這段代碼看起來沒有構造方法,

但是在編譯器生成的默認的構造方法里,就會包含這段初始化的代碼。第一個局部變量正好是this引用,

於是aload_0把this引用壓到操作數棧中。aload_0是aload_指令集中的一條,這組指令會將引用加載到操作數棧中。

n對應的是局部變量數組中的位置,並且也只能是0,1,2,3。還有類似的加載指令,它們加載的並不是對象引用,

比如iload_,lload_,fload_,和dload_, 這里i代表int,l代表long,f代表float,d代表double。

局部變量的在數組中的位置大於3的,得通過iload,lload,fload,dload,和aload進行加載,

這些指令都接受一個操作數,它代表的是要加載的局部變量的在數組中的位置。

invokespecial

這條指令可以用來調用對象實例的構造方法,私有方法和父類中的方法。

它是方法調用指令集中的一條,其它的還有invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual.

這里的invokespecial指令調用的是父類也就是java.lang.Object的構造方法。

bipush 它是用來把一個字節作為整型壓到操作數棧中的,在這里100會被壓到操作數棧里。
putfield

它接受一個操作數,這個操作數引用的是運行時常量池里的一個字段,在這里這個字段是simpleField。

賦給這個字段的值,以及包含這個字段的對象引用,在執行這條指令的時候,都 會從操作數棧頂上pop出來。

前面的aload_0指令已經把包含這個字段的對象壓到操作數棧上了,而后面的bipush又把100壓到棧里。

最后putfield指令會將這兩個值從棧頂彈出。執行完的結果就是這個對象的simpleField這個字段的值更新成了100。

上述代碼執行的時候內存里面是這樣的:

這里的putfield指令的操作數引用的是常量池里的第二個位置。JVM會為每個類型維護一個常量池,

運行時的數據結構有點類似一個符號表,盡管它包含的信息更多。Java中的字節碼操作需要對應的數據,

但通常這些數據都太大了,存儲在字節碼里不適合,它們會被存儲在常量池里面,

而字節碼包含一個常量池里的引用 。當類文件生成的時候,其中的一塊就是常量池:

 1 Constant pool:
 2    #1 = Methodref          #4.#16         //  java/lang/Object."<init>":()V
 3    #2 = Fieldref           #3.#17         //  SimpleClass.simpleField:I
 4    #3 = Class              #13            //  SimpleClass
 5    #4 = Class              #19            //  java/lang/Object
 6    #5 = Utf8               simpleField
 7    #6 = Utf8               I
 8    #7 = Utf8               <init>
 9    #8 = Utf8               ()V
10    #9 = Utf8               Code
11   #10 = Utf8               LineNumberTable
12   #11 = Utf8               LocalVariableTable
13   #12 = Utf8               this
14   #13 = Utf8               SimpleClass
15   #14 = Utf8               SourceFile
16   #15 = Utf8               SimpleClass.java
17   #16 = NameAndType        #7:#8          //  "<init>":()V
18   #17 = NameAndType        #5:#6          //  simpleField:I
19   #18 = Utf8               LSimpleClass;
20   #19 = Utf8               java/lang/Object
常量字段(類常量)

帶有final標記的常量字段在class文件里會被標記成ACC_FINAL.

比如

1 public class SimpleClass {
2 
3     public final int simpleField = 100;
4 
5 }

字段的描述信息會標記成ACC_FINAL:

1 public static final int simpleField = 100;
2     Signature: I
3     flags: ACC_PUBLIC, ACC_FINAL
4     ConstantValue: int 100

對應的初始化代碼並不變:

1 4: aload_0
2 5: bipush        100
3 7: putfield      #2                  // Field simpleField:I
靜態變量

帶有static修飾符的靜態變量則會被標記成ACC_STATIC:

1 public static int simpleField;
2     Signature: I
3     flags: ACC_PUBLIC, ACC_STATIC

不過在實例的構造方法中卻再也找不到對應的初始化代碼了。

因為static變量會在類的構造方法中進行初始化,並且它用的是putstatic指令而不是putfiled。

1 static {};
2   Signature: ()V
3   flags: ACC_STATIC
4   Code:
5     stack=1, locals=0, args_size=0
6        0: bipush         100
7        2: putstatic      #2                  // Field simpleField:I
8        5: return

未完待續。

本文最早發表於本人個人博客:Java譯站

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM