我們知道Java代碼編譯後生成的是字節碼,那虛擬機是如何加載這些class字節碼文件的呢?加載之後又是如何進行方法調用的呢?
Java有一個口號叫做一次編寫,到處運行
。實現這個口號的就是可以運行在不同平台上的虛擬機和與平台無關的字節碼。這裡要注意的是,虛擬機也是中立的,只要是符合規范的字節碼,都可以被虛擬機接受,例如Groovy,JRuby等語言,都會生成符合規范的字節碼,然後被虛擬機所運行,虛擬機不關心字節碼由哪種語言生成。
class類文件是一組以8位字節為基礎的二進制流,它包含以下幾個部分:
魔數和class文件版本:類文件開頭的四個字節被定義為CAFEBABE,只有開頭為CAFEBABE的文件才可以被虛擬機接受,接下來四個字節為class文件的版本號,高版本JDK可以兼容以前版本的class文件,但不能運行以後版本的class文件。
常量池:可以理解為class文件中的資源倉庫,它包含兩大類常量:字面量和符號引用,字面量包含文本字符串,聲明為final的常量值等,符號引用包含類和接口的全限定名,字段的名稱和描述符,方法的名稱和描述符。
訪問標志:常量池結束後,緊接著兩個字節表示訪問標志,用於識別一些類或接口層次的訪問信息,例如是否是public,是否是static等。
類索引,父類索引,和接口索引集合:類索引用來確定這個類的全限定名,父類為父類的全限定名,接口索引集合為接口的全限定名。
字段表集合:用於描述接口或者類中聲明的變量,但不包含方法中的變量。
方法表集合:用於表述接口或者類中的方法。
屬性表集合:class文件,字段表,方法表中的屬性都源自這裡。
虛擬機把描述類的數據從class文件加載到內存,並對數據進行校驗,轉換分析和初始化,最終形成可以被虛擬節直接使用的JAVA類型,這就是虛擬機的類加載機制。
類從被加載到虛擬機內存到卸載出內存的生命周期包括:加載->連接(驗證->准備->解析)->初始化->使用->卸載。
- 使用new關鍵字實例化對象時,讀取或設置一個類的靜態字段,除被final修飾經編譯結果放在常量池的靜態字段,調用類的靜態方法時。
- 使用java.lang.reflect包方法對類進行反射調用時。(Class.forName())。
- 初始化子類時,如果父類沒有初始化。
- 虛擬機啟動時main方法所在的類。
- 當使用JDK1.7動態語言支持時,java.lang.invoke.MethodHandle實例解析結果為REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,且對應類沒有進行初始化。
加載
加載是類加載的第一個階段,虛擬機要完成以下三個過程:
- 通過類的全限定名獲取定義此類的二進制字節流。
- 將字節流的存儲結構轉化為方法區的運行時結構。
- 在內存中生成一個代表該類的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();
}
}
}
結果為:
可以看到,由自定義的加載類只能獲取同包下的class,而系統的class不能被加載,而且由Class.forName()獲取的類與自定義加載類得到的類不是同一個類。
根據五種初始化的條件,父類也會被初始化,但是,上邊的代碼運行結果顯示,父類和接口都沒有被初始化,這又是怎麼回事呢?
系統提供了三種類加載器,分別是:
我們自定義的ClassLoader繼承自應用程序類加載器,當自定義類加載器找不到所加在的類時,會使用啟動類加載器進行加載,當啟動類加載器加載不到時,由擴展類加載,擴展類加載不到時有應用程序類加載。這也是為什麼上邊的代碼能夠成功運行的原因。
http://www.linuxidc.com/Linux/2015-07/120682.htm 中講到虛擬機棧是線程私有的,線程中會為運行的方法創建棧幀。
棧幀是虛擬機棧的棧元素,棧幀存儲了局部變量表,操作數棧,動態連接,返回地址等信息。每一個方法的調用都對應著一個棧幀在虛擬機棧中的入棧和出棧。
局部變量表
由方法參數,方法內定義的局部變量組成,容量以變量槽(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語言的方法調用分為靜態多分派,動態單分派。