英文原文鏈接,譯文鏈接,原文作者: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譯站