[深入理解JVM 一]Java程序執(zhí)行流程

本篇是《深入理解JVM》系列博客的第一篇,旨在全局把控,先對整體流程有個認識,然后再分階段詳解.本篇博客大部分內(nèi)容來自http://www.cnblogs.com/dqrcsc/p/4671879.htmljava一些地方重新進行了整理,根據(jù)自己的理解重新規(guī)劃了內(nèi)容—TML

概述

程序執(zhí)行流程我把它劃分為以下幾個步驟:編輯源碼、編譯生成class文件、(加載class文件、運行class字節(jié)碼文件),其中后兩個步驟都是在jvm虛擬機上執(zhí)行的。

執(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:

20170809114544464.png

部分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
,有具體描述,這里簡單說下。

20170809115320310.png

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)的接口


20170816143551888.png

直到第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();
}
20170816143933834.png

Mximum stack depth指定當前方法即main()方法對應(yīng)棧幀中的操作數(shù)棧的最大深度,當前值為5

Maximum local variables指定main()方法中局部變量表的大小,當前為2,及有兩個slot用于存放方法的參數(shù)及局部變量。

Code length指定main()方法中代碼的長度。

20170816144555899.png

執(zhí)行過程如下:

1.為main方法創(chuàng)建棧幀:

20170816144303175.png

局部變量表長度為2,slot0存放參數(shù)args,slot1存放局部變量Student s,操作數(shù)棧最大深度為5。

2. new#7指令,在java堆中創(chuàng)建一個Student對象,并將其引用值放入棧頂。

20170816144615371.png

3.初始化一個對象(通過實例構(gòu)造的方式)

up指令:復(fù)制棧頂?shù)闹?,然后將?fù)制的結(jié)果入棧。

bipush 23:將單字節(jié)常量值23入棧。

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

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

20170816144830251.png

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>()方法中。

20170816144946109.png

注意到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>()方法的棧幀:

20170816145524342.png

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

20170816145627113.png

aload_0:將局部變量表slot0處的引用值入棧

aload_1:將局部變量表slot1處的int值入棧

aload_2:將局部變量表slot2處的引用值入棧

20170816145755714.png

putfield #2:將當前棧頂?shù)闹怠?0150723”賦值給0x2222所引用對象的sid字段,然后棧中的兩個值出棧。

return:返回調(diào)用方即main()方法,當前方法棧幀出棧。
重新回到main()方法中,繼續(xù)執(zhí)行下面的字節(jié)碼指令:

astore_1:將當前棧頂引用類型的值賦值給slot1處的局部變量,然后出棧。

20170816150925158.png

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入棧

20170816151022684.png

6. 開始執(zhí)行第二行代碼,也就是strudy方法

invokevirtual #11:調(diào)用虛方法study(),這個方法是重寫的接口中的方法,需要動態(tài)分派,所以使用了invokevirtual指令。

創(chuàng)建study()方法的棧幀:

20170816151434396.png

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

20170816151526635.png

方法的java源碼:

public int study(int a, int b){
    int c = 10;
    int d = 20;
    return a+b*c-d;
}

20170816151708837.png

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這三個局部變量入棧:

20170816151839398.png

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

20170816151924992.png

iadd:將當前棧頂?shù)膬蓚€值出棧,相加的結(jié)果入棧

iload 4:將slot4處的int型的局部變量入

20170816152018858.png

isub:將棧頂兩個值出棧,相減結(jié)果入棧:

ireturn:將當前棧頂?shù)闹捣祷氐秸{(diào)用方。

20170816152046502.png

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 第6章類文件結(jié)構(gòu) 6.1 概述 6.2 無關(guān)性基石 6.3 Class類文件的結(jié)構(gòu) java虛擬機不和包括java...
    kennethan閱讀 1,070評論 0 2
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,728評論 18 399
  • MP4格式是現(xiàn)在播放器通用的視頻格式之一,這種格式的視頻兼容特別好,幾乎所有的播放器都支持MP4格式的視頻,而用迅...
    視頻轉(zhuǎn)換閱讀 961評論 0 0
  • 幾天前的“山竹”過去了,頭一日廣州天氣清涼舒爽,我抬頭觀望天藍的那一刻心里特別的清晰涌出一個字“戒”! “戒”從心...
    郎五升閱讀 861評論 0 0
  • 嗯,一定要找到那一個能讓你的心懶下來的人,從此不在劍拔弩張左右奔突;也一定要找到那一個能讓你的心精進起來的人,從此...
    洪心閱讀 831評論 0 4

友情鏈接更多精彩內(nèi)容