本篇是《深入理解JVM》系列博客的第一篇,旨在全局把控,先對整體流程有個認識,然后再分階段詳解.本篇博客大部分內(nèi)容來自http://www.cnblogs.com/dqrcsc/p/4671879.htmljava一些地方重新進行了整理,根據(jù)自己的理解重新規(guī)劃了內(nèi)容—TML
概述
程序執(zhí)行流程我把它劃分為以下幾個步驟:編輯源碼、編譯生成class文件、(加載class文件、運行class字節(jié)碼文件),其中后兩個步驟都是在jvm虛擬機上執(zhí)行的。

編輯
過程描述
編輯源代碼,就是我們在任何一個工具上編寫源代碼,可以是記事本,最后命名為Student.java。
這部分相當于我們在myeclipse這樣的ide上新建一個.java的Class然后寫內(nèi)容。
源碼文件
class Person {
private String name;
private int age;
public Person(int age, String name){
this.age = age;
this.name = name;
}
public void run(){
}
}
interface IStudyable {
public int study(int a, int b);
}
//public類,與java文件同名
public class Student extends Person implements IStudyable {
private static int cnt=5;
static{
cnt++;
}
private String sid;
public Student(int age, String name, String sid){
super(age,name);
this.sid = sid;
}
public void run(){
System.out.println("run()...");
}
public int study(int a, int b){
int c = 10;
int d = 20;
return a+b*c-d;
}
public static int getCnt(){
return cnt;
}
public static void main(String[] args){
Student s = new Student(23,"dqrcsc","20150723");
s.study(5,6);
Student.getCnt();
s.run();
}
}
編譯
過程描述
生成.class字節(jié)碼文件,輸入命令javac Student.java將該源碼文件編譯生成.class字節(jié)碼文件。由于在源碼文件中定義了兩個類,一個接口,所以生成了3個.clsss文件。
這部分的操作就相當于我們在myeclipse這樣的ide上寫完代碼ctrl+s保存
字節(jié)碼文件
字節(jié)碼文件,看似很微不足道的東西,卻真正實現(xiàn)了java語言的跨平臺。各種不同平臺的虛擬機都統(tǒng)一使用這種相同的程序存儲格式。更進一步說,jvm運行的是class字節(jié)碼文件,只要是這種格式的文件就行,所以,實際上jvm并不像我之前想象地那樣與java語言緊緊地捆綁在一起。如果非常熟悉字節(jié)碼的格式要求,可以使用二進制編輯器自己寫一個符合要求的字節(jié)碼文件,然后交給jvm去運行;或者把其他語言編寫的源碼編譯成字節(jié)碼文件,交給jvm去運行,只要是合法的字節(jié)碼文件,jvm都會正確地跑起來。所以,它還實現(xiàn)了跨語言……下面是一個字節(jié)碼文件Student.class.txt:

部分class文件內(nèi)容,從上面圖中,可以看到這些信息來自于Student.class,編譯自Student.java,編譯器的主版本號是52,也就是jdk1.8,這個類是public,然后是存放類中常量的常量池,各個方法的字節(jié)碼等
它存放了這個類的各種信息:字段、方法、父類、實現(xiàn)的接口等各種信息。
運行
過程描述
在命令行中輸入java Student這個命令,就啟動了一個java虛擬機,然后加載Student.class字節(jié)碼文件到內(nèi)存,然后運行內(nèi)存中的字節(jié)碼指令了。
這部分的操作就相當于我們在myeclipse這樣的ide上點擊運行按鈕
JVM基本結(jié)構(gòu)介紹
Jvm的運行時內(nèi)存分區(qū)和溢出處理見我的本系列第二篇博文(java底層分析—jvm內(nèi)存分析)http://blog.csdn.net/sinat_33087001/article/details/76976027
,有具體描述,這里簡單說下。

JVM中把內(nèi)存分為方法區(qū)、Java棧、Java堆、本地方法棧、PC寄存器5部分數(shù)據(jù)區(qū)域。
方法區(qū):用于存放類、接口的元數(shù)據(jù)信息,加載進來的字節(jié)碼數(shù)據(jù)都存儲在方法區(qū)
Java棧(虛擬機棧):執(zhí)行引擎運行字節(jié)碼時的運行時內(nèi)存區(qū),采用棧幀的形式保存每個方法的調(diào)用運行數(shù)據(jù)
本地方法棧:執(zhí)行引擎調(diào)用本地方法時的運行時內(nèi)存區(qū)
Java堆(堆):運行時數(shù)據(jù)區(qū),各種對象一般都存儲在堆上
PC寄存器(程序計數(shù)器):功能如同CPU中的PC寄存器,指示要執(zhí)行的字節(jié)碼指令。
JVM的功能模塊主要包括類加載器、執(zhí)行引擎和垃圾回收系統(tǒng)。
類加載
加載階段
1)類加載器會在指定的classpath中找到Student.class(通過類的全限定名)這個文件,然后讀取字節(jié)流中的數(shù)據(jù),將其存儲在方法區(qū)中。
2)會根據(jù)Student.class的信息建立一個Class對象,這個對象比較特殊,一般也存放在方法區(qū)中,用于作為運行時訪問Student類的各種數(shù)據(jù)的接口。
驗證階段:
3)必要的驗證工作,格式、語義等
準備階段:
4)為Student中的靜態(tài)字段分配內(nèi)存空間,也是在方法區(qū)中,并進行零初始化,即數(shù)字類型初始化為0,boolean初始化為false,引用類型初始化為null等。
private static int cnt=5;
此時,并不會執(zhí)行賦值為5的操作,而是將其初始化為0。
解析階段
5)由于已經(jīng)加載到內(nèi)存了,所以原來字節(jié)碼文件中存放的部分方法、字段等的符號引用可以解析為其在內(nèi)存中的直接引用了,而不一定非要等到真正運行時才進行解析。
初始化階段
6)由于已經(jīng)加載到內(nèi)存了,所以原來字節(jié)碼文件中存放的部分方法、字段等的符號引用可以解析為其在內(nèi)存中的直接引用了,而不一定非要等到真正運行時才進行解析。
在Student.java中只有一個靜態(tài)字段:
一個類加載之前要加載它的父類及其實現(xiàn)的接口

直到第390行才看到自己定義的部分被加載,先是Student實現(xiàn)的接口IStudyable,然后是其父類Person,然后才是Student自身,然后是一個啟動類的加載,然后就是找到main()方法,執(zhí)行了。
運行字節(jié)碼指令
執(zhí)行引擎找到main()這個入口方法,執(zhí)行其中的字節(jié)碼指令:
只有當前正在運行的方法的棧幀位于棧頂,當前方法返回,則當前方法對應(yīng)的棧幀出棧,當前方法的調(diào)用者的棧幀變?yōu)闂m敚?strong>當前方法的方法體中若是調(diào)用了其他方法,則為被調(diào)用的方法創(chuàng)建棧幀,并將其壓入棧頂。
簡單查看Student.main()的運行過程:
public static void main(String[] args){
Student s = new Student(23,"dqrcsc","20150723");
s.study(5,6);
Student.getCnt();
s.run();
}

Mximum stack depth指定當前方法即main()方法對應(yīng)棧幀中的操作數(shù)棧的最大深度,當前值為5
Maximum local variables指定main()方法中局部變量表的大小,當前為2,及有兩個slot用于存放方法的參數(shù)及局部變量。
Code length指定main()方法中代碼的長度。

執(zhí)行過程如下:
1.為main方法創(chuàng)建棧幀:

局部變量表長度為2,slot0存放參數(shù)args,slot1存放局部變量Student s,操作數(shù)棧最大深度為5。
2. new#7指令,在java堆中創(chuàng)建一個Student對象,并將其引用值放入棧頂。

3.初始化一個對象(通過實例構(gòu)造的方式)
up指令:復(fù)制棧頂?shù)闹?,然后將?fù)制的結(jié)果入棧。
bipush 23:將單字節(jié)常量值23入棧。
ldc #8:將#8這個常量池中的常量即”dqrcsc”取出,并入棧。
ldc #9:將#9這個常量池中的常量即”20150723”取出,并入棧。

4. invokespecial #10:調(diào)用#10這個常量所代表的方法,即Student.()這個方法,這步是為了初始化對象s的各項值
<init>()方法,是編譯器將調(diào)用父類的<init>()的語句、構(gòu)造代碼塊、實例字段賦值語句,以及自己編寫的構(gòu)造方法中的語句整合在一起生成的一個方法。保證調(diào)用父類的<init>()方法在最開頭,自己編寫的構(gòu)造方法語句在最后,而構(gòu)造代碼塊及實例字段賦值語句按出現(xiàn)的順序按序整合到<init>()方法中。

注意到Student.<init>()方法的最大操作數(shù)棧深度為3,局部變量表大小為4。
此時需注意:從dup到ldc #9這四條指令向棧中添加了4個數(shù)據(jù),而Student.()方法剛好也需要4個參數(shù):
public Student(int age, String name, String sid){
super(age,name);
this.sid = sid;
}
雖然定義中只顯式地定義了傳入3個參數(shù),而實際上會隱含傳入一個當前對象的引用作為第一個參數(shù),所以四個參數(shù)依次為this,age,name,sid。
上面的4條指令剛好把這四個參數(shù)的值依次入棧,進行參數(shù)傳遞,然后調(diào)用了Student.<init>()方法,會創(chuàng)建該方法的棧幀,并入棧。棧幀中的局部變量表的第0到4個slot分別保存著入棧的那四個參數(shù)值。
創(chuàng)建Studet.<init>()方法的棧幀:

Student.<init>()方法中的字節(jié)碼指令:

aload_0:將局部變量表slot0處的引用值入棧
aload_1:將局部變量表slot1處的int值入棧
aload_2:將局部變量表slot2處的引用值入棧

putfield #2:將當前棧頂?shù)闹怠?0150723”賦值給0x2222所引用對象的sid字段,然后棧中的兩個值出棧。
return:返回調(diào)用方即main()方法,當前方法棧幀出棧。
重新回到main()方法中,繼續(xù)執(zhí)行下面的字節(jié)碼指令:
astore_1:將當前棧頂引用類型的值賦值給slot1處的局部變量,然后出棧。

5. 到這兒為止,第一行代碼執(zhí)行完畢,將s返回給局部變量表,執(zhí)行下邊的
public static void main(String[] args){
Student s = new Student(23,"dqrcsc","20150723");//執(zhí)行完畢
s.study(5,6);
Student.getCnt();
s.run();
}
aload_1:slot1處的引用類型的值入棧
iconst_5:將常數(shù)5入棧,int型常數(shù)只有0-5有對應(yīng)的iconst_x指令
bipush 6:將常數(shù)6入棧

6. 開始執(zhí)行第二行代碼,也就是strudy方法
invokevirtual #11:調(diào)用虛方法study(),這個方法是重寫的接口中的方法,需要動態(tài)分派,所以使用了invokevirtual指令。
創(chuàng)建study()方法的棧幀:

最大棧深度3,局部變量表5

方法的java源碼:
public int study(int a, int b){
int c = 10;
int d = 20;
return a+b*c-d;
}

bipush 10:將10入棧
istore_3:將棧頂?shù)?0賦值給slot3處的int局部變量,即c,出棧。
bipush 20:將20入棧
istore 4:將棧頂?shù)?0付給slot4處的int局部變量,即d,出棧。
上面4條指令,完成對c和d的賦值工作。
iload_1、iload_2、iload_3這三條指令將slot1、slot2、slot3這三個局部變量入棧:

imul:將棧頂?shù)膬蓚€值出棧,相乘的結(jié)果入棧:

iadd:將當前棧頂?shù)膬蓚€值出棧,相加的結(jié)果入棧
iload 4:將slot4處的int型的局部變量入

isub:將棧頂兩個值出棧,相減結(jié)果入棧:
ireturn:將當前棧頂?shù)闹捣祷氐秸{(diào)用方。

7. 到這兒為止,第二行代碼執(zhí)行完畢,返回值返回給s,執(zhí)行下邊的
public static void main(String[] args){
Student s = new Student(23,"dqrcsc","20150723");//執(zhí)行完畢
s.study(5,6);
Student.getCnt();
s.run();
}
invokestatic #12 調(diào)用靜態(tài)方法getCnt()不需要傳任何參數(shù)
pop:getCnt()方法有返回值,將其出棧
aload_1:將slot1處的引用值入棧
invokevirtual #13:調(diào)用0x2222對象的run()方法,重寫自父類的方法,需要動態(tài)分派,所以使用invokevirtual指令
return:main()返回,程序運行結(jié)束。
總結(jié)
總結(jié)起來,一個類文件首先加載到方法區(qū),一些符號引用被解析(靜態(tài)解析)為直接引用或者等到運行時分派(動態(tài)綁定),經(jīng)過一系列的加載過程(class文件的常量池被加載到方法區(qū)的運行時常量池,各種其它的靜態(tài)存儲結(jié)構(gòu)被加載為方法區(qū)運行時數(shù)據(jù)解構(gòu)等等)
然后程序通過Class對象來訪問方法區(qū)里的各種類型數(shù)據(jù),當加載完之后,程序發(fā)現(xiàn)了main方法,也就是程序入口,那么程序就在棧里創(chuàng)建了一個棧幀,逐行讀取方法里的代碼所轉(zhuǎn)換為的指令,而這些指令大多已經(jīng)被解析為直接引用了,那么程序通過持有這些直接引用使用指令去方法區(qū)中尋找變量對應(yīng)的字面量來進行方法操作。
操作完成后方法返回給調(diào)用方,該棧幀出棧。內(nèi)存空間被GC回收,堆里被new的那些也就被來及回收機制GC了。
全流程包括以下幾步:源碼編寫–編譯(javac編譯和jit編譯,java語法糖)—類文件被加載到虛擬機(類Class文件結(jié)構(gòu),虛擬機運行時內(nèi)存分析,類加載機制)—-虛擬機執(zhí)行二進制字節(jié)碼(虛擬機字節(jié)碼執(zhí)行系統(tǒng))—垃圾回收(JVM垃圾回收機制)
分別對應(yīng)我的其它7篇博客,這是該系列博客的第一篇
這就是一個java程序從編寫,編譯,到運行的全流程。各部分可參考的我同系列的博文鏈接,這里再次感謝這篇博文所講,讓我茅塞頓開http://www.cnblogs.com/dqrcsc/p/4671879.html