1. 程式人生 > >[深入理解JVM 一]---Java程式執行流程

[深入理解JVM 一]---Java程式執行流程

本篇是《深入理解JVM》系列部落格的第一篇,旨在全域性把控,先對整體流程有個認識,然後再分階段詳解.本篇部落格大部分內容來自http://www.cnblogs.com/dqrcsc/p/4671879.htmljava一些地方重新進行了整理,根據自己的理解重新規劃了內容—TML

概述

程式執行流程我把它劃分為以下幾個步驟:編輯原始碼、編譯生成class檔案、(載入class檔案、執行class位元組碼檔案),其中後兩個步驟都是在jvm虛擬機器上執行的。
這裡寫圖片描述

編輯

過程描述

編輯原始碼,就是我們在任何一個工具上編寫原始碼,可以是記事本,最後命名為Student.java。

這部分相當於我們在myeclipse這樣的ide上新建一個.java的Class

然後寫內容。

原始碼檔案

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位元組碼檔案,輸入命令javac Student.java將該原始碼檔案編譯生成.class位元組碼檔案。由於在原始碼檔案中定義了兩個類,一個介面,所以生成了3個.clsss檔案。

這部分的操作就相當於我們在myeclipse這樣的ide上寫完程式碼ctrl+s儲存

位元組碼檔案

位元組碼檔案,看似很微不足道的東西,卻真正實現了java語言的跨平臺。各種不同平臺的虛擬機器都統一使用這種相同的程式儲存格式。更進一步說,jvm執行的是class位元組碼檔案,只要是這種格式的檔案就行,所以,實際上jvm並不像我之前想象地那樣與java語言緊緊地捆綁在一起。如果非常熟悉位元組碼的格式要求,可以使用二進位制編輯器自己寫一個符合要求的位元組碼檔案,然後交給jvm去執行或者把其他語言編寫的原始碼編譯成位元組碼檔案,交給jvm去執行,只要是合法的位元組碼檔案,jvm都會正確地跑起來。所以,它還實現了跨語言……下面是一個位元組碼檔案Student.class.txt:

這裡寫圖片描述
部分class檔案內容,從上面圖中,可以看到這些資訊來自於Student.class,編譯自Student.java,編譯器的主版本號是52,也就是jdk1.8,這個類是public,然後是存放類中常量的常量池,各個方法的位元組碼等

它存放了這個類的各種資訊:欄位、方法、父類、實現的介面等各種資訊。

執行

過程描述

在命令列中輸入java Student這個命令,就啟動了一個java虛擬機器,然後載入Student.class位元組碼檔案到記憶體,然後執行記憶體中的位元組碼指令了。

這部分的操作就相當於我們在myeclipse這樣的ide上點選執行按鈕

JVM基本結構介紹

Jvm的執行時記憶體分割槽和溢位處理見我的本系列第二篇博文(java底層分析—jvm記憶體分析)http://blog.csdn.net/sinat_33087001/article/details/76976027,有具體描述,這裡簡單說下。
這裡寫圖片描述
JVM中把記憶體分為方法區、Java棧、Java堆、本地方法棧、PC暫存器5部分資料區域。
方法區:用於存放類、介面的元資料資訊,載入進來的位元組碼資料都儲存在方法區
Java棧(虛擬機器棧):執行引擎執行位元組碼時的執行時記憶體區,採用棧幀的形式儲存每個方法的呼叫執行資料
本地方法棧:執行引擎呼叫本地方法時的執行時記憶體區
Java堆():執行時資料區,各種物件一般都儲存在堆上
PC暫存器(程式計數器):功能如同CPU中的PC暫存器,指示要執行的位元組碼指令。

JVM的功能模組主要包括類載入器、執行引擎垃圾回收系統

類載入

載入階段
1)類載入器會在指定的classpath中找到Student.class(通過類的全限定名)這個檔案,然後讀取位元組流中的資料,將其儲存在方法區中。
2)會根據Student.class的資訊建立一個Class物件,這個物件比較特殊,一般也存放在方法區中,用於作為執行時訪問Student類的各種資料的介面。
驗證階段
3)必要的驗證工作,格式、語義等
準備階段
4)為Student中的靜態欄位分配記憶體空間,也是在方法區中,並進行零初始化,即數字型別初始化為0,boolean初始化為false,引用型別初始化為null等。

              private static int cnt=5; 

此時,並不會執行賦值為5的操作,而是將其初始化為0。
解析階段
5)由於已經載入到記憶體了,所以原來位元組碼檔案中存放的部分方法、欄位等的符號引用可以解析為其在記憶體中的直接引用了,而不一定非要等到真正執行時才進行解析。
初始化階段
6)由於已經載入到記憶體了,所以原來位元組碼檔案中存放的部分方法、欄位等的符號引用可以解析為其在記憶體中的直接引用了,而不一定非要等到真正執行時才進行解析。
在Student.java中只有一個靜態欄位:

一個類載入之前要載入它的父類及其實現的介面

這裡寫圖片描述

直到第390行才看到自己定義的部分被載入,先是Student實現的介面IStudyable,然後是其父類Person,然後才是Student自身,然後是一個啟動類的載入,然後就是找到main()方法,執行了。

執行位元組碼指令

執行引擎找到main()這個入口方法,執行其中的位元組碼指令:
只有當前正在執行的方法的棧幀位於棧頂,當前方法返回,則當前方法對應的棧幀出棧,當前方法的呼叫者的棧幀變為棧頂;當前方法的方法體中若是呼叫了其他方法,則為被呼叫的方法建立棧幀,並將其壓入棧頂。

簡單檢視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()方法對應棧幀中的運算元棧的最大深度,當前值為5

Maximum local variables指定main()方法中區域性變量表的大小,當前為2,及有兩個slot用於存放方法的引數及區域性變數。

Code length指定main()方法中程式碼的長度。
這裡寫圖片描述

執行過程如下:

1,為main方法建立棧幀

這裡寫圖片描述

區域性變量表長度為2,slot0存放參數args,slot1存放區域性變數Student s,運算元棧最大深度為5。

2,new#7指令,在java堆中建立一個Student物件,並將其引用值放入棧頂。

這裡寫圖片描述

3,初始化一個物件(通過例項構造的方式)

up指令:複製棧頂的值,然後將複製的結果入棧。

bipush 23:將單位元組常量值23入棧。

ldc #8:將#8這個常量池中的常量即”dqrcsc”取出,併入棧。

ldc #9:將#9這個常量池中的常量即”20150723”取出,併入棧。

這裡寫圖片描述

4,invokespecial #10:呼叫#10這個常量所代表的方法,即Student.()這個方法,這步是為了初始化物件s的各項值

<init>()方法,是編譯器將呼叫父類的<init>()的語句、構造程式碼塊、例項欄位賦值語句,以及自己編寫的構造方法中的語句整合在一起生成的一個方法。保證呼叫父類的<init>()方法在最開頭,自己編寫的構造方法語句在最後,而構造程式碼塊及例項欄位賦值語句按出現的順序按序整合到<init>()方法中。

這裡寫圖片描述

注意到Student.<init>()方法的最大運算元棧深度為3,區域性變量表大小為4。

此時需注意:從dup到ldc #9這四條指令向棧中添加了4個數據,而Student.()方法剛好也需要4個引數:

public Student(int age, String name, String sid){

              super(age,name);

              this.sid = sid;

}

雖然定義中只顯式地定義了傳入3個引數,而實際上會隱含傳入一個當前物件的引用作為第一個引數,所以四個引數依次為this,age,name,sid。

上面的4條指令剛好把這四個引數的值依次入棧,進行引數傳遞,然後呼叫了Student.<init>()方法,會建立該方法的棧幀,併入棧。棧幀中的區域性變量表的第0到4個slot分別儲存著入棧的那四個引數值。

建立Studet.<init>()方法的棧幀:

這裡寫圖片描述

Student.<init>()方法中的位元組碼指令:

這裡寫圖片描述
aload_0:將區域性變量表slot0處的引用值入棧

aload_1:將區域性變量表slot1處的int值入棧

aload_2:將區域性變量表slot2處的引用值入棧

這裡寫圖片描述

invokespecial #1:呼叫Person.()方法,同調用Student.過程類似,建立棧幀,將三個引數的值存放到區域性變量表等,這裡就不畫圖了……

從Person.()返回之後,用於傳參的棧頂的3個值被回收了。

aload_0:將slot0處的引用值入棧。

aload_3:將slot3處的引用值入棧。

這裡寫圖片描述

putfield #2:將當前棧頂的值”20150723”賦值給0x2222所引用物件的sid欄位,然後棧中的兩個值出棧。

return:返回呼叫方即main()方法,當前方法棧幀出棧。
重新回到main()方法中,繼續執行下面的位元組碼指令:

astore_1:將當前棧頂引用型別的值賦值給slot1處的區域性變數,然後出棧。

這裡寫圖片描述

5,到這兒為止,第一行程式碼執行完畢,將s返回給區域性變量表,執行下邊的

   public static void main(String[] args){

              Student s = new Student(23,"dqrcsc","20150723");//執行完畢

              s.study(5,6);

              Student.getCnt();

              s.run();

}

aload_1:slot1處的引用型別的值入棧

iconst_5:將常數5入棧,int型常數只有0-5有對應的iconst_x指令

bipush 6:將常數6入棧

這裡寫圖片描述

6,開始執行第二行程式碼,也就是strudy方法

invokevirtual #11:呼叫虛方法study(),這個方法是重寫的介面中的方法,需要動態分派,所以使用了invokevirtual指令。

建立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:將棧頂的10賦值給slot3處的int區域性變數,即c,出棧。

bipush 20:將20入棧

istore 4:將棧頂的20付給slot4處的int區域性變數,即d,出棧。

上面4條指令,完成對c和d的賦值工作。

iload_1、iload_2、iload_3這三條指令將slot1、slot2、slot3這三個區域性變數入棧:

這裡寫圖片描述

imul:將棧頂的兩個值出棧,相乘的結果入棧:

這裡寫圖片描述

iadd:將當前棧頂的兩個值出棧,相加的結果入棧

iload 4:將slot4處的int型的區域性變數入
這裡寫圖片描述

isub:將棧頂兩個值出棧,相減結果入棧:

ireturn:將當前棧頂的值返回到呼叫方。

這裡寫圖片描述

7,到這兒為止,第二行程式碼執行完畢,返回值返回給s,執行下邊的

   public static void main(String[] args){

              Student s = new Student(23,"dqrcsc","20150723");//執行完畢

              s.study(5,6);

              Student.getCnt();

              s.run();

}

invokestatic #12 呼叫靜態方法getCnt()不需要傳任何引數

pop:getCnt()方法有返回值,將其出棧

aload_1:將slot1處的引用值入棧

invokevirtual #13:呼叫0x2222物件的run()方法,重寫自父類的方法,需要動態分派,所以使用invokevirtual指令

return:main()返回,程式執行結束。

總結

總結起來,一個類檔案首先載入到方法區,一些符號引用被解析(靜態解析)為直接引用或者等到執行時分派(動態繫結),經過一系列的載入過程(class檔案的常量池被載入到方法區的執行時常量池,各種其它的靜態儲存結構被載入為方法區執行時資料解構等等)

然後程式通過Class物件來訪問方法區裡的各種型別資料,當載入完之後,程式發現了main方法,也就是程式入口,那麼程式就在棧裡建立了一個棧幀,逐行讀取方法裡的程式碼所轉換為的指令,而這些指令大多已經被解析為直接引用了,那麼程式通過持有這些直接引用使用指令去方法區中尋找變數對應的字面量來進行方法操作。

操作完成後方法返回給呼叫方,該棧幀出棧。記憶體空間被GC回收,堆裡被new的那些也就被來及回收機制GC了。

全流程包括以下幾步:原始碼編寫–編譯(javac編譯和jit編譯,java語法糖)—類檔案被載入到虛擬機器(類Class檔案結構,虛擬機器執行時記憶體分析,類載入機制)—-虛擬機器執行二進位制位元組碼(虛擬機器位元組碼執行系統)—垃圾回收(JVM垃圾回收機制)

分別對應我的其它7篇部落格,這是該系列部落格的第一篇
這就是一個java程式從編寫,編譯,到執行的全流程。各部分可參考的我同系列的博文連結,這裡再次感謝這篇博文所講,讓我茅塞頓開http://www.cnblogs.com/dqrcsc/p/4671879.html