JAVA虛擬機器(4)筆記,java虛擬機器筆記
在 C 語言裡面我們想執行一段自己編寫的機器指令的方法大概如下:
| |
typedef void (*FUNC)(int);char* str = "your code";FUNC f = (FUNC)str; |
也就是說,我們完全可以做一個工具,從一個檔案中讀入指令,然後將這些指令運行起來。上面代碼中“編好的機器指令”當然指的是能在CPU上啟動並執行,如果這裡我還實現了一個翻譯機器:從自己定義的格式指令翻譯到CPU指令,那麼就可以執行根據自訂格式的代碼了。那麼上面這段代碼是不是相當於最簡單的一個虛擬機器了?下面來看JVM的總體結構:
ClassLoader的作用是裝載能被JVM識別的指令(當然不只是從磁碟檔案或記憶體去裝載),那麼我們先瞭解一下該格式:
魔數以及版本就不說了(滿大街的檔案格式都是這個東西),接著的便是常量池,其中無非是兩種東西:
1. 字面常量(比如Integer、Long、String等);
2. 符號引用(方法是哪裡的?什麼樣的?);
而我們知道,在JVM裡面Class都是根據全限定名去找的,那麼方法的描述當然也應該如此,那麼就得到這些常量之間的關係如下:
在接下來的“存取權限”中表明了該Class是public還是private等,而this&super&interface則表面了“本類”、“繼承自哪個類”、“實現了哪些介面”,實際上這裡只是儲存了代表這些資訊的CONSTANT_Class_info的下標(u2)。
感覺這裡的NameIndex和DescriptorIndex加起來和NameAndType有點像,那麼為什麼不直接用一個NameAndType的索引值表示?MethodInfo和FieldInfo之間最大的不同點就是Attributes。比如FieldInfo的屬性工作表中存放的是變數的初始值,而MethodInfo的屬性工作表中存放的則是位元組碼。那麼我們來依次看這些Attributes,首先是Code:
有幾個有意思的地方:
1. 從Class檔案中可以知道在執行的過程中棧的深度;
2. 對於非靜態方法,編譯器會將this通過參數傳遞給方法;
3. 異常表中記錄的範圍是指令的行數(而不是原始碼的);
4. 這裡的異常是指try-catch中的,而與Code同級的異常表中的則是指throws出去的;
Exceptions則非常簡單:
LineNumberTable儲存了位元組碼和源碼之間的關係,結構如下:
LocalVariableTable描述了棧幀中局部變數表的變數和原始碼中定義的變數之間的關係,結構如下:
SourceFile指明了產生該Class檔案的Java源碼檔案名稱(比如在一個Java檔案中申明了很多類的時候會產生很多Class檔案),結構如下:
Deprecated和Synthetic屬性只存在“有”和“沒有”的區別:
1. Deprecated:被程式作者定為不再推薦使用,通過@deprecated注釋說明;
2. Synthetic:表示欄位或方法是由編譯器自動產生的,比如<init>;
這也就是為什麼Code屬性後面會有Attribute的原因?
類載入的時機就很簡單了:在用到的時候就載入(廢話!)。下來看一下類載入的過程:
執行上面這段過程的是:ClassLoader,這個東西還是非常重要的,在JVM中是通過ClassLoader和類本身共同去判斷兩個Class是否相同。換句話說就是:不同的ClassLoader載入同一個Class檔案,那麼JVM認為他們產生的類是不同的。有些時候不會從Class檔案中載入流(比如Java Applet是從網路中載入),那麼這個ClassLoader和普通的實現邏輯當然是不一樣的,通過不同的ClassLoader就可以解決這個問題。
但是允許使用不同的ClassLoader又引發了新的問題:如果我也聲明了一個java.lang.Integer,但是裡面的代碼非常危險,怎麼辦?這裡就引出了雙親委派模式:
除了頂層的啟動類載入器外,其餘的類載入器都應該有父類載入器(通過組合實現),它在接到載入類的請求時優先委派給父類載入器去完成。
這樣的話,在載入java.lang.Integer的時候會優先使用系統的類載入器,這樣就不會載入使用者自己寫的。在Java程式員看到有3種系統提供的類載入器:
1. Bootstrap ClassLoader:負責載入<JAVA_HOME>\lib目錄中的類庫,無法被Java程式直接引用;
2. Extension ClassLoader:負責載入<JAVA_HOME>\lib\ext,開發人員可以直接使用;
3. Application ClassLoader:載入ClassPath上所指定的類庫,如果沒有自己定義過自己的類載入器則會使用它;
這樣預設的類會是有Application ClassLoader去載入類,然後如果發現要使用新的類型的時候則會遞迴地使用Application ClassLoader去載入(在前面的載入過程中提到)。這樣,只有在自己的程式中能使用自己編寫的ClassLoader去載入類,並且這個被載入的類是不能被別人使用的。
雙親委派模式不是一個強制性的約束,而是Java設計者推薦給開發人員的類載入實現方式。雙親委派模式出現過的3次“破壞”:
1. 為了相容JDK 1.0,建議使用者去覆蓋findClass方法;
2. 在基礎類要訪問使用者類的代碼會出現問題(比如JNDI):線程山下文類載入器;
3. 使用者的一些需求,比如HotSwap、OSGI等;
載入完完成後,接下來就要看程式是怎麼啟動並執行。棧幀是用於支援虛擬機器進行方法調用和執行,幀的意思就是一個單位,在調用其他方法的時候會向棧中壓入棧幀,結構如下:
在Class檔案編譯完成之後,在啟動並執行時候需要多少個局部變數就已經確定(在前面Class檔案中也已經看到過了),那麼這裡需要注意這個特性可能會引發GC(具體如何引發就不在這裡細說了)。在棧中,總是底層的棧去調用高層的棧(並且一定的相鄰的),那麼他們在參數傳遞(返回結果)的時往往是通過將其壓入運算元棧,有些虛擬機器為了提高這部分的效率使得相鄰棧幀“糾纏”在一起:
那麼我們接下來要去看是方法是如何執行的,第一個問題就是執行哪個方法?在“面向過程”的編程中似乎不存在在個問題,但是在Java OR C++中這都是比較蛋疼的一個問題。原因就是平時不會這麼用,但是你必須去搞明白= =。JVM確定目標方法的時候有兩種方法:
1. 靜態指派:根據參數類型和方法名稱來決定調用哪個方法。但是,並不是說沒有發現匹配的類型就報錯,比如有:func(int a),而在調用func(‘a’)的時候也會調用該方法(當然是在沒有func(char a)的前提下),這樣給人的關鍵就有點像一個處理的鏈條。不管多麼複雜,這些都是在編譯期間確定的,因為這裡是向上找的。
2. 動態指派:最普遍的就是Interface a = new Implements(),a調用方法到底應該是哪個類的在編譯期間是無法確定的。其實動態指派實現起來也很簡單:在調用方法的時候先拿到對象的實際類型。
其實“靜態”和“動態”給人的感覺還是比較模糊的,“靜態指派”給人的感覺是根據參數的類型向上尋找方法,“動態指派”給人的感覺則是根據執行個體的真實類型向上尋找。虛擬機器最佳化動態指派的效率一般是為類在方法區中建立一個虛方法表:
虛方法表中存放各個方法實際入口地址,如果某個方法在子類中沒有被重寫,那麼子類的虛方法表裡面的地址入口和父類相同方法的地址入口是一致的,都指向父類的實現入口。如果子類重寫了這個方法,子類方法表中的地址將會被替換為指向子類實現版本的入口地址。其實往簡單裡說,就是一個預先處理。
著作權聲明:感覺我寫的還算不錯的的話希望你能夠動動你的滑鼠和鍵盤為我點上一個贊或是為我奉獻上一個評論,在下感激不盡!_______________________________________________________歡迎轉載,希望在你轉載的同時,添加原文地址,謝謝配合