《深入理解Java虛擬機(jī)》筆記
在C里面我們想執(zhí)行一段自己編寫的機(jī)器指令的方法大概如下:
- typedef void(*FUNC)(int);
- char* str = "your code";
- FUNC f = (FUNC)str;
- (*f)(0);
也就是說,我們完全可以做一個(gè)工具,從一個(gè)文件中讀入指令,然后將這些指令運(yùn)行起來。上面代碼中“編好的機(jī)器指令”當(dāng)然指的是能在CPU上運(yùn)行的,如果這里我還實(shí)現(xiàn)了一個(gè)翻譯機(jī)器:從自己定義的格式指令翻譯到CPU指令,那么就可以執(zhí)行根據(jù)自定義格式的代碼了。那么上面這段代碼是不是相當(dāng)于最簡單的一個(gè)虛擬機(jī)了?下面來看JVM的總體結(jié)構(gòu):
ClassLoader的作用是裝載能被JVM識(shí)別的指令(當(dāng)然不只是從磁盤文件或內(nèi)存去裝載),那么我們先了解一下該格式:
魔數(shù)以及版本就不說了(滿大街的文件格式都是這個(gè)東西),接著的便是常量池,其中無非是兩種東西:
- 字面常量(比如Integer、Long、String等);
- 符號(hào)引用(方法是哪里的?什么樣的?);
而我們知道,在JVM里面Class都是根據(jù)全限定名去找的,那么方法的描述當(dāng)然也應(yīng)該如此,那么就得到這些常量之間的關(guān)系如下:
在接下來的“訪問權(quán)限”中表明了該Class 是public還是private等,而this&super&interface則表面了“本類”、“繼承自哪個(gè)類”、“實(shí)現(xiàn)了哪些接口”,實(shí)際上這里只是保存了代表這些信息的CONSTANT_Class_info的下標(biāo)(u2)。
感覺這里的NameIndex和 DescriptorIndex加起來和NameAndType有點(diǎn)像,那么為什么不直接用一個(gè)NameAndType的索引值表示?MethodInfo和FieldInfo之間最大的不同點(diǎn)就是Attributes。比如FieldInfo的屬性表中存放的是變量的初始值,而 MethodInfo的屬性表中存放的則是字節(jié)碼。那么我們來依次看這些Attributes,首先是Code:
有幾個(gè)有意思的地方:
- 從Class文件中可以知道在執(zhí)行的過程中棧的深度;
- 對(duì)于非靜態(tài)方法,編譯器會(huì)將this通過參數(shù)傳遞給方法;
- 異常表中記錄的范圍是指令的行數(shù)(而不是源代碼的);
- 這里的異常是指try-catch中的,而與Code同級(jí)的異常表中的則是指throws出去的;
Exceptions則非常簡單:
LineNumberTable保存了字節(jié)碼和源碼之間的關(guān)系,結(jié)構(gòu)如下:
LocalVariableTable描述了棧幀中局部變量表的變量和源代碼中定義的變量之間的關(guān)系,結(jié)構(gòu)如下:
SourceFile指明了生成該Class文件的Java源碼文件名(比如在一個(gè)Java文件中申明了很多類的時(shí)候會(huì)生成很多Class文件),結(jié)構(gòu)如下:
Deprecated和Synthetic屬性只存在“有”和“沒有”的區(qū)別:
- Deprecated:被程序作者定為不再推薦使用,通過@deprecated注釋說明;
- Synthetic:表示字段或方法是由編譯器自動(dòng)生成的,比如<init>;
這也就是為什么Code屬性后面會(huì)有Attribute的原因?
類加載的時(shí)機(jī)就很簡單了:在用到的時(shí)候就加載(廢話!)。下來看一下類加載的過程:
執(zhí)行上面這段過程的是:ClassLoader,這個(gè)東西還是非常重要的,在JVM中是通過ClassLoader和類本身共同去判斷兩個(gè)Class是否相同。換句話說就是:不同的ClassLoader加載同一個(gè)Class文件,那么JVM認(rèn)為他們生成的類是不同的。有些時(shí)候不會(huì)從Class文件中加載流(比如Java Applet是從網(wǎng)絡(luò)中加載),那么這個(gè)ClassLoader和普通的實(shí)現(xiàn)邏輯當(dāng)然是不一樣的,通過不同的ClassLoader就可以解決這個(gè)問題。
但是允許使用不同的ClassLoader又引發(fā)了新的問題:如果我也聲明了一個(gè)java.lang.Integer,但是里面的代碼非常危險(xiǎn),怎么辦?這里就引出了雙親委派模式:
除了頂層的啟動(dòng)類加載器外,其余的類加載器都應(yīng)該有父類加載器(通過組合實(shí)現(xiàn)),它在接到加載類的請(qǐng)求時(shí)優(yōu)先委派給父類加載器去完成。
這樣的話,在加載java.lang.Integer的時(shí)候會(huì)優(yōu)先使用系統(tǒng)的類加載器,這樣就不會(huì)加載用戶自己寫的。在Java程序員看到有3種系統(tǒng)提供的類加載器:
- Bootstrap ClassLoader:負(fù)責(zé)加載<JAVA_HOME>\lib目錄中的類庫,無法被Java程序直接引用;
- Extension ClassLoader:負(fù)責(zé)加載<JAVA_HOME>\lib\ext,開發(fā)者可以直接使用;
- Application ClassLoader:加載ClassPath上所指定的類庫,如果沒有自己定義過自己的類加載器則會(huì)使用它;
這樣默認(rèn)的類會(huì)是有Application ClassLoader去加載類,然后如果發(fā)現(xiàn)要使用新的類型的時(shí)候則會(huì)遞歸地使用Application ClassLoader去加載(在前面的加載過程中提到)。這樣,只有在自己的程序中能使用自己編寫的ClassLoader去加載類,并且這個(gè)被加載的類是不能被別人使用的。
雙親委派模式不是一個(gè)強(qiáng)制性的約束,而是Java設(shè)計(jì)者推薦給開發(fā)者的類加載實(shí)現(xiàn)方式。雙親委派模式出現(xiàn)過的3次“破壞”:
- 為了兼容JDK 1.0,建議使用者去覆蓋findClass方法;
- 在基礎(chǔ)類要訪問用戶類的代碼會(huì)出現(xiàn)問題(比如JNDI):線程山下文類加載器;
- 用戶的一些需求,比如HotSwap、OSGI等;
加載完完成后,接下來就要看程序是怎么運(yùn)行的。棧幀是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和執(zhí)行,幀的意思就是一個(gè)單位,在調(diào)用其他方法的時(shí)候會(huì)向棧中壓入棧幀,結(jié)構(gòu)如下:
在Class文件編譯完成之后,在運(yùn)行的時(shí)候需要多少個(gè)局部變量就已經(jīng)確定(在前面Class文件中也已經(jīng)看到過了),那么這里需要注意這個(gè)特性可能會(huì)引發(fā)GC(具體如何引發(fā)就不在這里細(xì)說了)。在棧中,總是底層的棧去調(diào)用高層的棧(并且一定的相鄰的),那么他們?cè)趨?shù)傳遞(返回結(jié)果)的時(shí)往往是通過將其壓入操作數(shù)棧,有些虛擬機(jī)為了提高這部分的效率使得相鄰棧幀“糾纏”在一起:
那么我們接下來要去看是方法是如何執(zhí)行的,第一個(gè)問題就是執(zhí)行哪個(gè)方法?在“面向過程”的編程中似乎不存在在個(gè)問題,但是在Java OR C++中這都是比較蛋疼的一個(gè)問題。原因就是平時(shí)不會(huì)這么用,但是你必須去搞明白= =。JVM確定目標(biāo)方法的時(shí)候有兩種方法:
- 靜態(tài)分派:根據(jù)參數(shù)類型和方法名稱來決定調(diào)用哪個(gè)方法。但是,并不是說沒有發(fā)現(xiàn)匹配的類型就報(bào)錯(cuò),比如有:func(int a),而在調(diào)用func('a')的時(shí)候也會(huì)調(diào)用該方法(當(dāng)然是在沒有func(char a)的前提下),這樣給人的關(guān)鍵就有點(diǎn)像一個(gè)處理的鏈條。不管多么復(fù)雜,這些都是在編譯期間確定的,因?yàn)檫@里是向上找的。
- 動(dòng)態(tài)分派:最普遍的就是Interface a = new Implements(),a調(diào)用方法到底應(yīng)該是哪個(gè)類的在編譯期間是無法確定的。其實(shí)動(dòng)態(tài)分派實(shí)現(xiàn)起來也很簡單:在調(diào)用方法的時(shí)候先拿到對(duì)象的實(shí)際類型。
其實(shí)“靜態(tài)”和“動(dòng)態(tài)”給人的感覺還是比較模糊的,“靜態(tài)分派”給人的感覺是根據(jù)參數(shù)的類型向上查找方法,“動(dòng)態(tài)分派”給人的感覺則是根據(jù)實(shí)例的真實(shí)類型向上查找。虛擬機(jī)優(yōu)化動(dòng)態(tài)分派的效率一般是為類在方法區(qū)中建立一個(gè)虛方法表:
虛方法表中存放各個(gè)方法實(shí)際入口地址,如果某個(gè)方法在子類中沒有被重寫,那么子類的虛方法表里面的地址入口和父類相同方法的地址入口是一致的,都指向父類的實(shí)現(xiàn)入口。如果子類重寫了這個(gè)方法,子類方法表中的地址將會(huì)被替換為指向子類實(shí)現(xiàn)版本的入口地址。其實(shí)往簡單里說,就是一個(gè)預(yù)處理。
原文鏈接:http://www.cnblogs.com/tianchi/archive/2012/11/11/2761631.html