大衆點評資深架構師分享—Java虛擬機類文件結構+字節碼執行引擎

大衆點評資深架構師分享—Java虛擬機類文件結構+字節碼執行引擎


導語java

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

一 類文件結構

無關性基石性能優化

java有一個口號叫作一次編寫,處處運行。實現這個口號的就是能夠運行在不一樣平臺上的虛擬機和與平臺無關的字節碼。這裏要注意的是,虛擬機也是中立的,只要是符合規範的字節碼,均可以被虛擬機接受,例如Groovy,JRuby等語言,都會生成符合規範的字節碼,而後被虛擬機所運行,虛擬機不關心字節碼由哪一種語言生成。bash

類文件結構架構

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

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

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

訪問標誌:常量池結束後,緊接着兩個字節表示訪問標誌,用於識別一些類或接口層次的訪問信息,例如是不是public,是不是static等。分佈式

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

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

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

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

二 類加載機制

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

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

初始化的5種狀況:

  • 使用new關鍵字實例化對象時,讀取或設置一個類的靜態字段,除被final修飾經編譯結果放在常量池的靜態字段,調用類的靜態方法時。
  • 使用java.lang.reflect包方法對類進行反射調用時。(Class.forName())。
  • 初始化子類時,若是父類沒有初始化。
  • 虛擬機啓動時main方法所在的類。
  • 當使用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繼承自應用程序類加載器,當自定義類加載器找不到所加在的類時,會使用啓動類加載器進行加載,當啓動類加載器加載不到時,由擴展類加載,擴展類加載不到時有應用程序類加載。這也是爲何上邊的代碼可以成功運行的緣由。

在這裏給你們推薦一個Java技術交流羣:710373545裏面會分享一些資深架構師錄製的視頻資料:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多!

三 字節碼執行引擎

運行時棧幀結構

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語言的方法調用分爲靜態多分派,動態單分派。

相關文章
相關標籤/搜索