前面學習了Class文件結構、類的加載機制、字節碼執行引擎、對象的創建與銷燬,所以我準備從一個Java代碼進行切入,詳細剖析它的生命歷程,將所學的知識真正的用起來,也算是對前面所學的知識進行一個系統的總結。
我們以這份Java代碼爲例,來剖析一個Java程序的生命歷程:
interface ClassName {
String getClassName();
}
class Company implements ClassName {
String className;
public Company(String className) {
this.className = className;
}
@Override
public String getClassName() {
return className;
}
}
public class Main {
public static void main(String[] args) {
String className;
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
className = scanner.next();
Company company = new Company(className);
System.out.println("name=" + company.getClassName());
}
}
}
可以看到,這份代碼涉及到了接口,繼承,對象的實例化,main方法,值得我們花費一些功夫去從JVM層面上了解這個程序從編譯、運行到結束都發生了哪些事情。
所以,別急,讓我們按順序慢慢來分析。
編譯階段
首先你要運行一個java程序,肯定要對其進行編譯,生成我們前面說的Class文件,這段代碼會生成3個Class文件。
Class文件中保存了魔數、版本符號、常量池、方法標誌、類索引、父類索引、接口索引、字段表(有可能含有屬性表)、方法表(有可能含有屬性表)等信息。這些信息具體的組成結構,我在這裏不再贅述。
我們可以通過字節碼文件,清晰的描述出Java源碼中有關類的所有信息。
在這裏只以Main類爲例,使用javap命令看一下生成的Class文件。
javap -verbose Main;
Last modified 2017-12-13; size 852 bytes
MD5 checksum 0336fa14cc04a9c858c34cc016880c19
Compiled from "Main.java"
public class Main
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #18.#29 // java/lang/Object."<init>":()V
#2 = Class #30 // java/util/Scanner
#3 = Fieldref #31.#32 // java/lang/System.in:Ljava/io/InputStream;
#4 = Methodref #2.#33 // java/util/Scanner."<init>":(Ljava/io/InputStream;)V
#5 = Methodref #2.#34 // java/util/Scanner.hasNext:()Z
#6 = Methodref #2.#35 // java/util/Scanner.next:()Ljava/lang/String;
#7 = Class #36 // Company
#8 = Methodref #7.#37 // Company."<init>":(Ljava/lang/String;)V
#9 = Fieldref #31.#38 // java/lang/System.out:Ljava/io/PrintStream;
#10 = Class #39 // java/lang/StringBuilder
#11 = Methodref #10.#29 // java/lang/StringBuilder."<init>":()V
#12 = String #40 // name=
#13 = Methodref #10.#41 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#14 = Methodref #7.#42 // Company.getClassName:()Ljava/lang/String;
#15 = Methodref #10.#43 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#16 = Methodref #44.#45 // java/io/PrintStream.println:(Ljava/lang/String;)V
#17 = Class #46 // Main
#18 = Class #47 // java/lang/Object
#19 = Utf8 <init>
#20 = Utf8 ()V
#21 = Utf8 Code
#22 = Utf8 LineNumberTable
#23 = Utf8 main
#24 = Utf8 ([Ljava/lang/String;)V
#25 = Utf8 StackMapTable
#26 = Class #30 // java/util/Scanner
#27 = Utf8 SourceFile
#28 = Utf8 Main.java
#29 = NameAndType #19:#20 // "<init>":()V
#30 = Utf8 java/util/Scanner
#31 = Class #48 // java/lang/System
#32 = NameAndType #49:#50 // in:Ljava/io/InputStream;
#33 = NameAndType #19:#51 // "<init>":(Ljava/io/InputStream;)V
#34 = NameAndType #52:#53 // hasNext:()Z
#35 = NameAndType #54:#55 // next:()Ljava/lang/String;
#36 = Utf8 Company
#37 = NameAndType #19:#56 // "<init>":(Ljava/lang/String;)V
#38 = NameAndType #57:#58 // out:Ljava/io/PrintStream;
#39 = Utf8 java/lang/StringBuilder
#40 = Utf8 name=
#41 = NameAndType #59:#60 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#42 = NameAndType #61:#55 // getClassName:()Ljava/lang/String;
#43 = NameAndType #62:#55 // toString:()Ljava/lang/String;
#44 = Class #63 // java/io/PrintStream
#45 = NameAndType #64:#56 // println:(Ljava/lang/String;)V
#46 = Utf8 Main
#47 = Utf8 java/lang/Object
#48 = Utf8 java/lang/System
#49 = Utf8 in
#50 = Utf8 Ljava/io/InputStream;
#51 = Utf8 (Ljava/io/InputStream;)V
#52 = Utf8 hasNext
#53 = Utf8 ()Z
#54 = Utf8 next
#55 = Utf8 ()Ljava/lang/String;
#56 = Utf8 (Ljava/lang/String;)V
#57 = Utf8 out
#58 = Utf8 Ljava/io/PrintStream;
#59 = Utf8 append
#60 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#61 = Utf8 getClassName
#62 = Utf8 toString
#63 = Utf8 java/io/PrintStream
#64 = Utf8 println
{
public Main();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 27: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=4, args_size=1
0: new #2 // class java/util/Scanner
3: dup
4: getstatic #3 // Field java/lang/System.in:Ljava/io/InputStream;
7: invokespecial #4 // Method java/util/Scanner."<init>":(Ljava/io/InputStream;)V
10: astore_2
11: aload_2
12: invokevirtual #5 // Method java/util/Scanner.hasNext:()Z
15: ifeq 63
18: aload_2
19: invokevirtual #6 // Method java/util/Scanner.next:()Ljava/lang/String;
22: astore_1
23: new #7 // class Company
26: dup
27: aload_1
28: invokespecial #8 // Method Company."<init>":(Ljava/lang/String;)V
31: astore_3
32: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
35: new #10 // class java/lang/StringBuilder
38: dup
39: invokespecial #11 // Method java/lang/StringBuilder."<init>":()V
42: ldc #12 // String name=
44: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
47: aload_3
48: invokevirtual #14 // Method Company.getClassName:()Ljava/lang/String;
51: invokevirtual #13 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
54: invokevirtual #15 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
57: invokevirtual #16 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
60: goto 11
63: return
LineNumberTable:
line 31: 0
line 32: 11
line 33: 18
line 34: 23
line 35: 32
line 36: 60
line 37: 63
StackMapTable: number_of_entries = 2
frame_type = 253 /* append */
offset_delta = 11
locals = [ top, class java/util/Scanner ]
frame_type = 51 /* same */
}
SourceFile: "Main.java"
我們大概分析一下上面的輸出結果吧:
直接從常量池開始分析,前面的幾行信息我認爲沒有分析的必要。
#1 = Methodref #18.#29 // java/lang/Object."<init>":()V
這是常量池裏面的第一項數據,#1代表索引,Methodref告訴我們常量池中第一個索引表示的是一個方法引用,對這個方法引用的描述用常量池中第18項和第29項的內容可以進行描述,你們可以查一下常量池中第18項和第29項的內容,其實對應的就是後面註釋的內容。它告訴了你這個方法所屬的類是Object,方法的簡單名稱是<init>,方法的描述符是()V,也就是Object中的實例構造函數(對簡單名稱和描述符我在前面的博客中已經進行了說明)。
你也許想問,爲什麼Main類常量池中的第一項數據描述的是Object類中的無參構造函數?你可能忘了,所有類都應如此,Java中所有的類都繼承自Object,常量池中也會保存他們父類的索引,因爲在Java中,對象的初始化與實例化不是還有一條規則嘛—先初始化與實例化父類,然後纔是子類(說的並不精確,明白我的意思就行)。
剩餘的常量池分析與上面類似。
常量池下面的代碼塊,可以看到,一個Main類的默認構造函數,一個就是main方法了。關於這兩個東西,我們等一下在字節碼的執行階段再說。
可以看到,Class文件中,包含着詳細的信息,有大量的信息都是你無法從源碼中直接得到的。
類加載階段
javac對源文件編譯完成,我們使用java命令開始運行這個Main類。java命令只能運行包含main方法的類。
java命令一開始運行,JVM開始對Main類進行加載。
這時候就對應了我們學習類加載機制的每個階段:加載(從.class文件中讀取字節碼)、驗證(對字節碼文件進行一系列的驗證保證格式無誤並且對JVM不會產生危害)、準備(爲類變量分配內存並進行系統初始化)、解析(分爲靜態鏈接與動態鏈接,非虛方法的符號引用會在這一階段被解析爲直接引用)、初始化(執行類構造器)。
我再對其過程進行一點點補充,如果想要更加詳細的說明,請移步至我的博客專欄。
JVM在加載這個Main類的時候,使用類加載器(雙親委派模型)對其進行加載,經歷了加載、驗證(在這個時候又會觸發Object類的加載)階段,由於在Main類中並沒有類變量,也就相當於跳過了準備這一階段,然後對字節碼進行解析,由於main方法是靜態方法,也就是非虛方法,開始靜態鏈接,在字節碼中直接將main方法的符號引用解析爲直接引用(別忘了Main類反編譯之後的默認構造器~),由於沒有靜態變量與靜態語句塊,所以初始化這一階段也相當於是直接跳過,最後整個加載過程完畢,並在方法區中生成Main類所對應的Class對象。
由於我這個例子中不涉及多態,也就不涉及分派,但這部分知識請務必掌握。
方法執行階段
類中所有的信息已經在內存中加載完畢,JVM開始進行方法調用… …
方法調用:JVM開始執行main方法,這部分工作是由虛擬機中字節碼執行引擎完成的。main方法會由一個線程進行調用。此線程會在虛擬機棧上爲自己開闢一部分的棧空間,此後只要這個線程調用新的方法,這個方法便會被當作棧幀壓入虛擬機棧的棧頂(作爲當前棧幀)。這個方法中定義的局部變量會被存儲進局部變量表,在JVM中,並不存儲局部變量的名稱,他們都是以局部變量表的相對偏移量來標識每個不同的局部變量。
我以main方法的Code屬性再說明一下棧幀中的局部變量表以及操作數棧。(Class文件中方法表的Code屬性保存的是Java方法體中的字節碼)
Code:
stack=3, locals=4, args_size=1
可以看到,main方法在調用之前(實際上在編譯階段,它的局部變量表,操作數棧的大小都已確定),locals爲4,也就是局部變量表的大小爲4:this,className,scanner,company;stack爲3,也就是操作數棧的大小3:className,scanner,company。
我們還可以在上面方法體的字節碼當中看到許多指令,而這些指令就是方法在執行的過程中,JVM需要解釋運行的,如下:
0: new #2 // class java/util/Scanner
前面的0表示的也是相對偏移量,而new指令就是新建一個對象,對應Java源碼中的new,後面的#2代表的是new指令的參數,表達的意思是常量池中索引爲2的數據項,也就是上述代碼後面註釋中的Scanner類。
值得一說的是,這裏的Scanner由於是對類的實例化,因此JVM會首先判斷Scanner這個類是否會被加載進內存,如果沒有被加載進內存,JVM開始對這個類進行類加載,過程如上述步驟,然後進行對象的初始化。
指令一條條的向下面執行(程序計數器),進入循環體,最終執行到這一步:Company company = new Company(className);
,也是一個對類的實例化,但它和上面的Scanner又有點不同,這個類不僅實現了ClassName接口,而且實例化的時候還進行了傳參。那麼何時會加載ClassName接口呢,博主根據查閱的資料,猜測是在Company類加載中的驗證階段會觸發其接口的加載,具體大家可以Google、baidu。
那麼如何實現參數的傳遞呢,相信大家應該是有印象的,它是在實例化Company類的過程中,執行了Company的構造方法,而構造方法本身又是一個方法,因此可以理解爲實參先進入被調用方法的操作數棧中,然後將操作數棧中的引用出棧賦值給被調用方法的局部變量表。
最後,代碼執行完畢,程序退出。