本系列是用來記錄《深刻理解Java虛擬機》這本書的讀書筆記。方便本身查看,也方便你們查閱。java
欲速則不達,欲達則欲速!數組
第六章:類文件結構安全
講完了自動內存管理,咱們來講說執行子系統。執行子系統講解的是JVM如何執行程序。數據結構
Class文件概述jvm
這篇咱們只講講Class文件。Class文件又名類文件或字節碼文件。javac將.java文件(源代碼)編譯成class文件(字節碼),jvm再將.class文件解釋成機器碼。ide
Class文件中包含的是java虛擬機指令集和符號表以及若干其它輔助信息。其是一組以8字節爲基礎單元的二進制流,沒有空隙存在。優化
其存儲數據的結構有兩種:無符號數和表。編碼
(1)無符號數是用來描述數字,索引引用,數量值或按照UTF-8編碼構成字符串值。屬於基本的數據類型,以u1,u2,u4,u8分別表明1個字節,2個字節,4個字節,8個字節spa
(2)表是由多個無符號數或其它表做爲數據項構成的複合數據類型,以「_info」結尾。翻譯
其特色是:在class文件中,哪一個字節表明什麼含義,長度是多少,前後順序如何,都不容許改變。
Class文件組成部分
對於Class的組成,在上圖中已經羅列的很清楚了。還需再對常量池進行一下強調:當虛擬機運行時,須要從常量池得到對應的符號引用,再在類建立時或運行時解析、翻譯到具體的內存地址之中。
第七章:類加載機制
1、類加載器
JVM的類加載是經過ClassLoader及其子類來完成的,類的層次關係和加載順序能夠由下圖來描述:
一、基本概念
類加載機制是把類的數據從Class文件加載到內存,並對數據進行校驗,轉換解析和初始化,最終造成能夠被虛擬機直接使用的java類型。這一系列的過程都是在程序運行期間完成的。
二、使用情景
對於一個非數組類的加載階段,可使用系統提供的引導類加載器來完成,也能夠由用戶自定義的類加載器去完成。
對於數組類而言,其由java虛擬機直接建立,不經過類加載器。
三、雙親委派機制
雙親委派機制是類加載所採起的一種方式。若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委託給父類加載器去完成。每一層的類加載器均是如此。只有當父加載器反饋本身沒法完成這個請求時,子加載器纔會嘗試本身去加載。
類比到現實:小明想買一個玩具推土機,可他又很差意思直接張口。因此,發生了下面的對話。
小明去問他爸爸:爸爸你有挖土機嗎?
爸爸說:沒有哎
接着爸爸問爺爺:爸爸爸爸,你有挖土機嗎?
爺爺說:沒有哎
接着爺爺問太爺爺:爸爸爸爸,你有挖土機嗎?
太爺爺說:我也沒有。讓重孫子去買一個吧。
結果小明就高高興興地本身去買了一個玩具挖土機。
問題來了:若是爺爺有一臺挖土機怎麼辦?那小明只能玩爺爺那個了,不能本身去買了。類比到類加載機制裏,就是若是某廣父類能對此類進行加載,那應用程序類或自定義這些子類就不用本身加載了。
四、分類
啓動類加載器是使用C++實現的,是虛擬機自身的一部分。
其它類加載器是由java語言實現的,獨立於虛擬機外部,而且所有繼承自抽象類java.lang.ClassLoader。
五、好處
以string類爲例,用戶本身寫了一個string類的實現,對此類進行加載時,只會委派給啓動類加載器來對JDK中本來的string類進行加載,而自定義的string類永遠不會被調用,這樣保證了系統的安全。
2、何時進行類加載
只有如下5中方式必須當即對類進行加載
一、使用new實例化對象的時候;讀取或配置一個類的靜態字段(被final修飾、已在編譯器把結果放入常量池的靜態字段除外)的時候;調用一個類的靜態方法的時候。
二、使用java.lang.reflect包的方法對類進行反射調用的時候。若是類沒有進行過初始化,則須要先觸發其初始化。
三、當初始化一個類的時候,若是發現其父類還沒進行過初始化,則須要先觸發其父類的初始化。
四、當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main方法的類),虛擬機會先初始化這個主類。
3、類加載過程詳述
類加載過程分爲5步。大部分都是由虛擬機主導和控制的,除了如下兩種情形:
開發人員能夠經過自定義類加載器參與
會執行開發人員的代碼去初始化變量和其它資源
一、加載
虛擬機須要完成的事情:
(1)經過一個類的全限定名來獲取定義此類的二進制字節流。
(2)將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
(3)在內存區生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口。
二、驗證
驗證的目的是確保Class文件的字節流中包含的信息符合當前虛擬機的要求,不會危害虛擬機自身的安全。
其分爲4個步驟:文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證。
其中文件格式驗證是直接對字節流進行操做的,其他3項是在方法區中進行的。
三、準備
此階段時正式爲類變量分配內存並設置類變量初始值的階段。其是在方法區中進行分配。有兩個注意點:
(1)此時只是對類變量(static修飾的變量)進行內存分配,而不是對象變量。給對象分配內存是在對象實例化時,隨着對象一塊兒分配到java堆中。
(2)若是一個類變量沒有被final修飾,則其初始值是數據類型的零值。好比int類型時0,boolean類型時false。舉個例子說明:
public static int value = 123;
在準備階段事後的初始值爲0而不是123,由於這個時候還沒有開始執行任何java方法,而把value賦值爲123的putstatic指令是程序被編譯後,存放於類加載器<clinit>()方法之中。因此把value賦值爲123的動做將在初始化階段纔會執行。
public static final int value=123;
此時由於final,因此在準備階段value就已經被賦值爲123了。
四、解析
解析階段時虛擬機將常量池內的符號引用替換爲直接引用的過程。可對類或接口、字段、類方法、接口方法等進行解析。
(1)符號引用是什麼
符號引用即便包含類的信息,方法名,方法參數等信息的字符串,它供實際使用時在該類的方法表中找到對應的方法。
(2)直接引用是什麼
直接引用就是偏移量,經過偏移量能夠直接在該類的內存區域中找到方法字節碼的起始位置。
符號引用時告訴你此方法的一些特徵,你須要經過這些特徵去找尋對應的方法。
直接引用就是直接告訴你此方法在哪。
五、初始化
此階段時用於初始化類變量和其它資源,是執行類構造器<clinit>()方法的過程,此時才真正開始執行勒種定義的java程序代碼。
第八章:字節碼執行引擎
JVM中的執行引擎在執行java代碼的時候,通常有解釋執行(經過解釋器執行)和編譯執行(經過即時編譯器產生本地代碼執行)兩種選擇。
1、棧幀
一、基本概念
(1)、定義
棧幀是用於支持虛擬機進行方法調用和方法執行的數據結構,它位於虛擬機棧裏面。
(2)、做用
每一個方法從調用開始到執行完成的過程當中,都對應着一個棧幀在虛擬機棧裏面從入棧到出棧的過程。
(3)、特色
二、局部變量表
局部變量表是一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。
//方法參數 max(int a,int b)
局部變量和類變量(用static修飾的變量)不一樣
//全局變量 int a; //局部變量 void say(){ int b=0; }
類變量有兩次賦初始值的過程:準備階段(賦予系統初始值)和初始化階段(賦予程序定義的初始值)。因此即便初始化階段沒有爲類變量賦值也不要緊,它仍然有一個肯定的初始值。
但局部變量不同,若是定義了,但沒賦初始值,是不能使用的。
三、操做棧
當一個方法剛剛開始執行的時候,這個方法的操做棧是空的,在方法的執行過程當中,會有各類字節碼指令往操做棧中寫入和提取內容,也就是出棧、入棧操做。
例如,計算:
int a = 2+3;
當執行iadd指令時,會將2和3出棧並相加,而後將相加的結果5出棧。
四、動態連接
Class文件的常量池中存有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用做爲參數。這些符號引用分爲兩個部分:
(1)靜態解析:在類加載階段或第一次使用的時候就轉化爲直接引用。
(2)動態連接:在每一次運行期間轉化爲直接引用。
五、返回地址
當一個方法開始執行後,只有兩種方式能夠退出這個方法:正常退出、異常退出。不管採用何種退出方式,在方法退出以後,都須要返回到方法被調用的位置,程序才能繼續執行。
(1)正常退出:調用者的PC計數器做爲返回地址,棧幀中通常會保存這個計數器值。
(2)異常退出:返回地址是經過異常處理器表來肯定的,棧幀中通常不會保存這部分信息。
2、方法調用
一、解析
對「編譯器可知,運行期不可變」的方法進行調用稱爲解析。符合這種要求的方法主要包括:
(1)靜態方法,用static修飾的方法
(2)私有方法,用private修飾的方法
二、分派
分派講解了虛擬機如何肯定正確的目標方法。分派分爲靜態分派和動態分派。講解靜動態分派以前,咱們先看個多態的例子。
Human man=new Man();
在這段代碼中,Human爲靜態類型,其在編譯期是可知的。Man是實際類型,結果在運行期纔可肯定,編譯期在編譯程序的時候並不知道一個對象的實際類型時什麼。
(1)靜態分派
全部依賴靜態類型來定位方法執行版本的分派動做稱爲靜態分派。它的典型應用是重載。
public class StaticDispatch{ static abstract class Human{ } static class Man extends Human{ } static class Woman extends Human{ } public void say(Human hum){ System.out.println("I am human"); } public void say(Man hum){ System.out.println("I am man"); } public void say(Woman hum){ System.out.println("I am woman"); } public static void main(String[] args){ Human man = new Man(); Human woman = new Woman(); StaticDispatch sr = new StaticDispatch(); sr.say(man); sr.say(woman); } }
運行結果是:
I am human I am human
爲何會產生這個結果呢?
由於編譯器在重載時,是經過參數的靜態類型而不是實際類型做爲判斷依據的。在編譯階段,javac編譯器會根據參數的靜態類型決定使用哪一個重載版本,因此兩個對say()方法的調用實際爲sr.say(Human)。
(2)動態分派
在運行期根據實際類型肯定方法執行版本的分派過程,它的典型應用是重寫。
public class DynamicDispatch{ static abstract class Human{ protected abstract void say(); } static class Man extends Human{ @Override protected abstract void say(){ System.out.println("I am man"); } } static class Woman extends Human{ @Override protected abstract void say(){ System.out.println("I am woman "); } } public static void main(String[] args){ Human man = new Man(); Human woman = new Woman(); man.say(); woman.say(); man=new Woman(); man.say(); } }
I am man I am woman I am woman
這彷佛纔是咱們平時敲的java代碼。對於方法重寫,在運行時才肯定調用哪一個方法。因爲Human的實際類型時man,所以調用的是man的name方法。其他的同理。
動態分派的實際依賴於方法區中的虛方法表,它裏面存放着各個方法的實際入口地址。若是某個方法在子類中被重寫了,那子類方法表中的地址將會替換爲指向子類實際版本的入口地址,不然,指向父類的實際入口。
(3)單分派和多分派
方法的接收者與放的參數統稱爲方法的宗量,分爲單分派和多分派。
單分派是指根據一個宗量就能夠知道調用目標(即應該調用哪一個方法),多分派須要根據多個宗量才能肯定調用目標。
在靜態分派中,須要調用者的實際類型和方法參數的類型才能肯定方法版本,因此其是多分派型。
在動態分派中,已經知道了參數的實際類型,因此此時只需知道方法調用者的實際類型就能夠肯定出方法版本,因此其是單分派類型。
綜上,java是一門靜態多分派,動態單分派的語言。
3、字節碼解釋執行引擎
虛擬機中的字節碼解釋執行引擎是基於棧的。下面經過一段代碼來仔細看一下其解釋的執行過程。
public int calc(){ int a = 100; int b = 200; int c = 300; return (a + b) * c; }
第一步:將100入棧。
第二步:將操做棧中的100出棧並存放到局部變量中,後面的200,300同理。
第三步:將局部變量表中的100複製到操做棧頂。
第四步:將局部變量表中的200複製到操做棧頂。
第五步:將100和200出棧,作整型加法,最後將結果300從新入棧。
第六步:將第三個數300從局部變量表複製到棧頂。接下來就是將兩個300出棧,進行整型乘法,將最後的結果90000入棧。
第七步:方法結束,將操做數棧頂的整型值返回給此方法的調用者。
鳴謝:特別感謝做者周志明提供的技術支持!