JAVA 虛擬機類加載機制和字節碼執行引擎

jopen 9年前發布 | 13K 次閱讀 Java Java開發

引言

    我們知道java代碼編譯后生成的是字節碼,那虛擬機是如何加載這些class字節碼文件的呢?加載之后又是如何進行方法調用的呢?


一 類文件結構

    無關性基石

    java有一個口號叫做一次編寫,到處運行。實現這個口號的就是可以運行在不同平臺上的虛擬機和與平臺無關的字節碼。這里要注意的是,虛擬機也是中立的,只要是符合規范的字節碼,都可以被虛擬機接受,例如Groovy,JRuby等語言,都會生成符合規范的字節碼,然后被虛擬機所運行,虛擬機不關心字節碼由哪種語言生成。

    類文件結構

    class類文件是一組以8位字節為基礎的二進制流,它包含以下幾個部分:

    魔數和class文件版本:類文件開頭的四個字節被定義為CAFEBABE,只有開頭為CAFEBABE的文件才可以被虛擬機接受,接下來四個字節為class文件的版本號,高版本JDK可以兼容以前版本的class文件,但不能運行以后版本的class文件。

    常量池:可以理解為class文件中的資源倉庫,它包含兩大類常量:字面量和符號引用,字面量包含文本字符串,聲明為final的常量值等,符號引用包含類和接口的全限定名,字段的名稱和描述符,方法的名稱和描述符。

    訪問標志:常量池結束后,緊接著兩個字節表示訪問標志,用于識別一些類或接口層次的訪問信息,例如是否是public,是否是static等。

    類索引,父類索引,和接口索引集合:類索引用來確定這個類的全限定名,父類為父類的全限定名,接口索引集合為接口的全限定名。

    字段表集合:用于描述接口或者類中聲明的變量,但不包含方法中的變量。

    方法表集合:用于表述接口或者類中的方法。

    屬性表集合:class文件,字段表,方法表中的屬性都源自這里。


二  類加載機制

    虛擬機把描述類的數據從class文件加載到內存,并對數據進行校驗,轉換分析和初始化,最終形成可以被虛擬節直接使用的JAVA類型,這就是虛擬機的類加載機制。

    類從被加載到虛擬機內存到卸載出內存的生命周期包括:加載->連接(驗證->準備->解析)->初始化->使用->卸載。

    初始化的5種情況:

  1. 使用new關鍵字實例化對象時,讀取或設置一個類的靜態字段,除被final修飾經編譯結果放在常量池的靜態字段,調用類的靜態方法時。

  2. 使用java.lang.reflect包方法對類進行反射調用時。(Class.forName())。

  3. 初始化子類時,如果父類沒有初始化。

  4. 虛擬機啟動時main方法所在的類。

  5. 當使用JDK1.7動態語言支持時,java.lang.invoke.MethodHandle實例解析結果為REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,且對應類沒有進行初始化。

    類加載過程

    加載

    加載是類加載的第一個階段,虛擬機要完成以下三個過程:1)通過類的全限定名獲取定義此類的二進制字節流。2)將字節流的存儲結構轉化為方法區的運行時結構。3)在內存中生成一個代表該類的Class對象,作為方法區各種數據的訪問入口。

    驗證

    目的是確保class文件字節流信息符合虛擬機的要求。

    準備

    為static修飾的變量賦初值,例如int型默認為0,boolean默認為false。

    解析

    虛擬機將常量池內的符號引用替換成直接引用。

    初始化

    初始化是類加載的最后一個階段,將執行類構造器<init>()方法,注意這里的方法不是構造方法。該方法將會顯式調用父類構造器,接下來按照java語句順序為類變量和靜態語句塊賦值。

    類加載器

    對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在java虛擬機中的唯一性。舉一個例子:

package com.sinaapp.gavinzhang.bean;
import java.io.InputStream;

public class App 
{
    public static void main( String[] args )
    {
        ClassLoader myClassLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try{
                    String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if(is == null)
                    {
                        System.out.println(fileName+ "is not find");
                        return super.loadClass(name);
                    }
                    System.out.println("fileName: "+fileName);
                    byte[] b = new byte[is.available()];
                    is.read(b);
                    return defineClass(name,b,0,b.length);
                }catch (Exception E)
                {
                    throw new ClassCastException(name);
                }

            }
        };
        try {
            Object obj = myClassLoader.loadClass("com.sinaapp.gavinzhang.bean.Resource").newInstance();
            Object obj1  = Class.forName("com.sinaapp.gavinzhang.bean.Resource").newInstance();
            System.out.println(obj instanceof com.sinaapp.gavinzhang.wesound.bean.Resource);
            System.out.println(obj1 instanceof com.sinaapp.gavinzhang.wesound.bean.Resource);
        }catch (Exception e)
        {
            e.printStackTrace();
        }
    }
}

    結果為:

JAVA 虛擬機類加載機制和字節碼執行引擎

    可以看到,由自定義的加載類只能獲取同包下的class,而系統的class不能被加載,而且由Class.forName()獲取的類與自定義加載類得到的類不是同一個類。

    根據五種初始化的條件,父類也會被初始化,但是,上邊的代碼運行結果顯示,父類和接口都沒有被初始化,這又是怎么回事呢?

    系統提供了三種類加載器,分別是:啟動類加載器(Bootstrap ClassLoader),該加載器會將<JAVA_HOME>\lib目錄下能被虛擬機識別的類加載到內存中。擴展類加載器(Extension ClassLoader),該加載器會將<JAVA_HOME>\lib\ext目錄下的類庫加載到內存。應用程序類加載器(Application ClassLoader),該加載器負責加載用戶路徑上所指定的類庫。

    我們自定義的ClassLoader繼承自應用程序類加載器,當自定義類加載器找不到所加在的類時,會使用啟動類加載器進行加載,當啟動類加載器加載不到時,由擴展類加載,擴展類加載不到時有應用程序類加載。這也是為什么上邊的代碼能夠成功運行的原因。


三  字節碼執行引擎

    運行時棧幀結構

    http://my.oschina.net/jiangmitiao/blog/470426  中講到虛擬機棧是線程私有的,線程中會為運行的方法創建棧幀。

JAVA 虛擬機類加載機制和字節碼執行引擎

    棧幀是虛擬機棧的棧元素,棧幀存儲了局部變量表,操作數棧,動態連接,返回地址等信息。每一個方法的調用都對應著一個棧幀在虛擬機棧中的入棧和出棧。

    局部變量表由方法參數,方法內定義的局部變量組成,容量以變量槽(Slot)為最小單位。如果該方法不是static方法,則局部變量表的第一個索引為該對象的引用,用this可以取到。

    操作數棧最開始為空,由字節碼指令往棧中存數據和取數據,方法的返回值也會存到上一個方法的操作數棧中。

    動態連接含有一個指向常量池中該棧幀所屬方法的引用,持有該引用是為了進行動態分派。

    方法返回地址存放的是調用該方法的pc計數器值,當方法正常返回時,就會把返回值傳遞到上層方法調用者。當方法中發生沒有可被捕獲的異常,也會返回,但是不會向上層傳遞返回值。

    

    方法調用

    java是一門面向對象的語言,它具有多態性。那么虛擬機又是如何知道運行時該調用哪一個方法?

    靜態分派是在編譯期就決定了該調用哪一個方法而不是由虛擬機來確定,方法重載就是典型的靜態分派。

    動態分派是在虛擬機運行階段才能決定調用哪一個方法,方法重寫就是典型的動態分派。

    動態分派的實現:當調用一個對象的方法時,會將該對象的引用壓棧到操作數棧,然后字節碼指令invokevirtual會去尋找該引用實際類型。如果在實際類型中找對應的方法,且訪問權限足夠,則直接返回該方法引用,否則會依照繼承關系對父類進行查找。實際上,如果子類沒有重寫父類方法,則子類方法的引用會直接指向父類方法。


    基于棧的字節碼執行引擎

    不管是解釋型語言還是編譯型語言,機器都無法理解非二進制語言。高級語言轉化成機器語言都遵循現代經典編譯原理。即執行前對程序源碼進行詞法和語法分析,構建抽象語法樹。C語言等編譯型語言會由單獨的執行引擎做這些工作,而Java語言等解釋型語言語法抽象樹由jvm完成。jvm可以選擇通過解釋器來解釋字節碼執行還是通過優化器生成機器代碼來執行。

    常用的兩套指令集架構分別是基于棧的指令集和基于寄存器的指令集。

    基于棧的指令集更多的通過入棧出棧來實現計算功能,例如1+1

    iconst_1  ;將1入棧
    iconst_1  ;將1入棧
    iadd      ;將棧頂兩個元素取出相加并將結果入棧

    基于寄存器的指令集更多的是使用寄存器來進行操作,例如1+1

mov eax,1 ;向eax中存1
 add eax,1 ;eax<-eax+1

   總體來說,基于棧的指令集會慢一些,但是它與寄存器無關,更容易實現到處運行的目標。


總結

    又到了該總結的時候了,類加載機制面試中很容易被問到,不幸的是,當時我并沒有看這方面的知識。

    class類文件結構的每一個部分都可以再深入下去,類文件結構是采用結構體的方式存儲的,那么怎么知道集合的長度,各個屬性又是怎么被標記的。

    類加載機制中有且僅有的五種觸發初始化的情況。類加載器的分類。

    棧幀的結構,以及方法調用。

    java語言的方法調用分為靜態多分派,動態單分派。

來自:http://my.oschina.net/jiangmitiao/blog/483824

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!