這篇文章的素材來自周志明的《深刻理解Java虛擬機》。java
做爲Java開發人員,必定程度瞭解JVM虛擬機的的運做方式很是重要,本文就一些簡單的虛擬機的相關概念和運做機制展開我本身的學習過程,是這個系列的第二篇。安全
咱們在文件裏寫入了java的源代碼,源代碼寫就後存入磁盤,磁盤上的源代碼通過javac命令的編譯造成了二進制字節碼造成了class文件,通過一番步驟後java虛擬機將這些二進制字節碼按照必定的方式讀入內存中的不一樣區域造成了二進制字節碼的活化狀態,虛擬機使用字節碼指定的命令執行這些指令,其間使用字節碼中存儲的數據,最終完成了任務。這個過程就是java虛擬機執行java二進制字節碼的過程的簡單歸納。能夠以下圖所示:數據結構
這只是對這個過程的簡單介紹,實際上其中的每一步都相當重要並且複雜,正是這些過程最終使得咱們編寫的java源代碼可以運行在虛擬機搭建的環境中。編輯器
java的二進制字節碼是一個緊密鏈接的二進制數碼,這個數碼的結構以下,各個結構之間是無縫鏈接的,也所以首先於這種規則,java的二進制代碼纔不會產生二義性,即虛擬機在讀區這些數碼時能夠惟一地解析出它所表達的意思。函數
這個龐大的結構主要包含如下幾個部分:工具
基本的信息用於肯定java二進制字節碼的特徵和加載可行特徵。
魔數「CAFEBABE」用以肯定這段字節碼是java字節碼的開始,版本號用於肯定不一樣版本的jdk編譯了不一樣版本的java源代碼生成了不一樣版本的二進制字節碼,這個標記的另外的目的用於提示虛擬機高於當前版本的二進制字節碼可能因爲兼容性不能加載。學習
全部和程序相關的常量都將加入這個部分中,這個部分開頭的常量數決定了常量池中常量的個數以使得虛擬機可以正確解析出哪些部分是常量池。後面的常量以表的形式呈現,「表」是字節碼中一個特殊的複合型的數據結構,不一樣類型的常量有不一樣的標記tag以指示虛擬機以不一樣的方式解析出常量的值。這樣最終虛擬機將根據不一樣類型的常量解析出常量池中的所有常量對應的值或索引。spa
常量分爲字面量和符號引用兩種,字面量即通常的基本類型的數據,好比整型、浮點型等,而符號引用則是那些須要進一步經過這個符號的值去尋找它真正引用的對象,好比CONSTANT_Fieldref_info類型的常量就是符號引用,必須經過這個字段名去尋找到它真正引用的字段。
以下是常量池中的常量類型,另外以CONSTANT_Utf8_info表爲例說明了常量表中的結構:設計
關乎類的訪問權限的信息將會以位的不一樣的形式展現在這裏。
如下是訪問標誌的不一樣位,若是有好幾個訪問標誌,那麼通常將它們作或運算將幾個相關的位都展現出來。3d
這些字節碼向虛擬機提供了這個類的類名、父類的類名和接口名的索引值,這個索引值最終將能夠從常量池中得到其對應的全限定名。
(成員變量的描述)這些字節碼向虛擬機提供了這個類中包含的字段的個數和每一個字段的信息,每一個字段一樣是用一個字段表來描述的,這個字段表裏說明了這個字段的信息:字段的訪問權限、名索引在常量池中找到它的名字、描述符說明了這個字段的類型,可能會附帶的屬性表則會進一步經過拓展的數據結構展現這個字段的其它屬性,好比這個字段可能被賦的初值。
如下展現的是字段表的結構:
(成員方法的描述)和字段表相似的,這些字節碼向虛擬機提供了這個類中包含的方法的個數和每一個方法的信息,,每一個方法用一個方法表來描述:方法的訪問權限、方法名的索引在常量池中找到這個方法的名字、描述符索引獲得了這個方法的特徵如返回值類型和參數,可能會附帶的屬性表則會進一步經過拓展的數據結構展現這個方法的其它屬性,好比這個方法索引獲得的Code屬性存在的話那麼說明這個方法的方法體是存在的,則接下去的字節碼就是具體的方法體了,這個方法體由Code屬性表來描述。Code屬性表則是更深刻的一個數據結構了(字節碼的數據結構就是以這樣可拓展的方式一步步創建的,當簡單的索引或字面量不足以描述的時候就會引入表,以結構化的方式來對所要描述的對象作進一步的闡釋),在Code屬性表裏規定了「Code」常量索引以肯定這段字節碼是方法體、Code屬性長度、最大棧、局部變量空間、代碼長、代碼、異常數和異常表,還有可能帶有其餘可拓展的屬性表。
如下是方法表的結構,針對方法表中的Code屬性表能夠看到它的更深一層的結構,方法表中還有其餘的屬性表可依據狀況以供拓展,好比Exceptions屬性表用以描述這個方法所要規定的可拋出異常。
基於一樣的拓展思想,總體結構最後也預留了一樣的屬性表來作拓展,包括源代碼所在文件等信息均可以拓展在這個部分裏。
這個部分咱們能夠清楚地看出字節碼設計者對於數據結構的可拓展性的追求,經過可拓展的屬性表的定義,不少難以描述的結構能夠更深一步的描述(這有點像是文件的結構:在一個文件難以描述的時候就用一個包含了不少文件的文件夾來共同描述),這種設計最終使java二進制字節碼可以長期穩定的存在下來,由於新添加的特性只須要在特定的節點作一個拓展便可。
上面的部分除了辛苦地使用十六進制編輯器對class文件做分析以外還能夠直接使用jdk提供的javap工具進行分析:javap -verbose * ,它將結構化的結果呈現出來:
jinhaoplus$ javap -verbose MyClass Classfile /Users/jinhao/Desktop/MyClass.class Last modified 2015-10-11; size 288 bytes MD5 checksum 8235b2e50d3ca6704b44862387570773 Compiled from "MyClass.java" class MyClass minor version: 0 major version: 52 flags: ACC_SUPER Constant pool: #1 = Methodref #5.#17 // java/lang/Object."<init>":()V #2 = String #18 // a #3 = Fieldref #4.#19 // MyClass.x:Ljava/lang/String; #4 = Class #20 // MyClass #5 = Class #21 // java/lang/Object #6 = Utf8 x #7 = Utf8 Ljava/lang/String; #8 = Utf8 ConstantValue #9 = Utf8 y #10 = Utf8 C #11 = Utf8 <init> #12 = Utf8 ()V #13 = Utf8 Code #14 = Utf8 LineNumberTable #15 = Utf8 SourceFile #16 = Utf8 MyClass.java #17 = NameAndType #11:#12 // "<init>":()V #18 = Utf8 a #19 = NameAndType #6:#7 // x:Ljava/lang/String; #20 = Utf8 MyClass #21 = Utf8 java/lang/Object { final java.lang.String x; descriptor: Ljava/lang/String; flags: ACC_FINAL ConstantValue: String a char y; descriptor: C flags: MyClass(); descriptor: ()V flags: Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: ldc #2 // String a 7: putfield #3 // Field x:Ljava/lang/String; 10: return LineNumberTable: line 1: 0 line 2: 4 } SourceFile: "MyClass.java"
java的字節碼通過了編譯存在了磁盤上,那麼把它從磁盤裏請到內存裏成爲真正活化可用的內存對象是相當重要的,這個過程稱之爲類加載過程:Class Loading,這個加載過程結束後class文件裏的二進制字節碼將會成爲內存不一樣區域裏的數據,虛擬機就能夠按照原則將這些數據表明的指令執行完成任務。
上圖展現的就是這個過程的分步驟。下面是這幾步的功能:
加載就是把二進制字節碼轉爲字節流,經過類的全限定名把對應class文件裏的二進制字節碼轉換爲虛擬機內存中方法區裏規定的數據結構(也就是說最終虛擬機裏的結構並非二進制字節碼的那種緊密型)以完成以後的數據向內存中的分配,同時在堆內存中開闢區域以存放類的java.lang.Class對象以使得未來造成的方法區中的數據能有入口訪問。
類加載器:加載的時候是根據全限定名去找到對應的二進制字節碼,這個過程是由類加載器完成的,雖然是同一個二進制字節碼文件,若是類加載器的選擇不一樣,那麼出於安全考慮也不能斷定加載出的類是徹底一致的,所以自定義加載器和系統的應用程序加載器對同一個類的加載結果是不同的,要斷定這兩個類是不同的。爲了解決這個問題,類加載器被設計成了多層繼承關係,從上向下分別是啓動類加載器、拓展類加載器、應用程序加載器、自定義加載器,加載的時候層層向上代理給父加載器,最終將會使得啓動類加載器執行最終的加載,以確保全部同名的類可以被同一個類加載器所加載。
畢竟是文件裏的字節碼,沒有辦法保證字節碼是不被修改的安全代碼,即便保證了沒被修改也不能保證代碼編寫者在知足編譯成功以外沒有犯下低級的語意錯誤,因此對字節碼的驗證工做相當重要,能夠必定程度上保證字節碼的安全性和正確性,虛擬機將從字節流的格式是否正確(是否知足class字節碼的格式限制)、元數據語法是否正確(類的元數據信息是否符合java語言的語法要求)、字節碼安全性是否保證(是否有跨越內存安全性的錯誤和隱患出現)、符號引用驗證是否可以經過(符號引用是查看那些非類自身的其它類和這些類中的字段和方法是否真的存在,這個過程是解析時會觸發的,解析的過程會去查看這些符號引用到的類的狀況是否會出錯)。
準備是爲了在方法區中爲即將要分配內存的數據開始開闢在相應位置開闢內存空間,並將相應的字節流注入到這些空間中去,同時爲字段賦初值。值得注意的是,除非字段帶有Constant Value屬性外,通常狀況下賦初值的時候都會爲字段賦零值。這個過程結束後方法區裏就已經創建起來了類的基本數據結構,這其中包括常量池的常量。
準備階段結束後類變量就都帶着初值在方法區中等待了,可是這個時候方法區中的常量池裏的常量卻只是一些字面量和符號引用,字面量是能夠直接使用的,可是符號引用必須轉換爲直接引用(能夠理解爲這些引用真正指向的地址)才能使用,不然這些常量的字面量並不能指定活化在內存裏的對象:好比常量池中有一個CONSTANT_Class_info類型的符號引用常量,這個類符號引用裏存儲的僅僅是類的全限定名的索引,找到全限定名以後也沒有什麼用處,由於沒辦法肯定符合這個全限定名的類在內存中加載的具體地址,所以必須將這個符號引用對應的類的直接引用(地址)找出來,也就是轉換爲直接引用。
因此解析的任務就是將常量池裏的符號引用轉換爲直接引用,以使得方法區裏的類、父類、接口、字段、方法可以經過自身的索引尋找常量池中的引用時直接定位到這些類、父類、接口、字段、方法的準確的內存地址。
另外須要注意的是,解析不必定非要在準備以後初始化以前進行,由於咱們能夠看到這個階段的主要任務是使用階段纔會用到的,若是程序中有動態綁定的需求時這時候是沒有辦法把符號引用準確轉換爲直接引用的。因此解析的階段有時會在初始化以後甚至使用的過程當中纔會再進行的。這樣作的好處就是可以完成相對靈活的動態綁定。
初始化的過程實際上就是執行<clinit>類初始化函數的過程,這個函數執行的其實就是字段的賦值語句和靜態代碼塊的執行,這步事後,全部的字段都將被初始化爲程序中賦值語句和靜態代碼塊要求的初值,而不是準備階段的零值。
以上幾個步驟就是類加載的全過程,在這個過程當中,class文件中的二進制字節碼以二進制字節流的形式先按照方法區特定的數據結構重整並創建java.lang.Class對象於堆中,驗證重整後的二進制字節流沒有語法、語意和安全性的問題後虛擬機爲即將加載的類在方法區中開闢內存空間,字節流注入開闢的方法區的內存空間並將各字段賦零值,常量池中的符號引用轉換爲有實際意義的直接引用以訪問特定的地址,特定的字段被初始化爲程序規定的初值,整個類成功加載到方法區中。
虛擬機規範制定了不少限制,這些限制是必須遵照的,不一樣的廠商對虛擬機的規範有不一樣的實現,可是面對限制都是同樣的遵照。java虛擬機對於類加載的時機沒有明確限制,可是對於類加載過程的初始化的時機卻有明確的四個「有且僅有要求」:這些條件下必須對類進行初始化,鑑於類初始化位於類加載過程的最後,因此這個規定也能夠大體理解爲類加載的時機,這些時機稱爲「主動引用」:
new新對象、讀寫靜態字段、調用靜態方法的時候必須初始化類:讀寫靜態字段時只初始化這個靜態字段所在的類,若是是父類的靜態字段則只初始化父類而不初始化子類;另外若是是static final修飾的靜態字段,那麼在編譯的時候就會將其寫入常量池,這個時候即便讀這個靜態字段也不會加載類,由於只須要去常量池中取這個值就好。這兩個策略的目的其實都是儘量地減小類加載的開銷;
反射調用的時候初始化類;
初始化類的時候若是父類未初始化要初始化父類;
執行主類(執行的main函數所在類)要初始化。