java虛擬機規範在日常工作中可以說根本用不到,但作爲一個完美主義者,感覺如果進入java這個行業,對它的方方面面不去掌握的話,未免有些遺憾,我沒有那些改寫java語言大師們的天賦,我只能站在他們的肩膀,來掌握他們創造的技術。
閒話不多說,我會認真讀java虛擬機並寫下自己理解或者有用的東西,看到的、寫下的、說出的纔是學到的,讀者可以去http://down.51cto.com/自行下載java虛擬機規範。
關於java虛擬機規範:
本規範描述的是一種抽象化的虛擬機的行爲,而不是任何一種(譯者注:包括Oracle公司自己的HotSpot和JRockit虛擬機)被廣泛使用的虛擬機實現。 如果只是“正確的”實現一臺java虛擬機,其實並不是所有人想的那麼困難和高深-只需要讀取Class文件每一條字節碼指令,並能正確的執行這些指令的操作即可。所有在虛擬機規範之中沒有明確描述的實現細節,都不應成爲虛擬機設計者發揮創造性的牽絆,設計者可以完全自主決定所有規範中不曾描述的虛擬機內部細節,例如:運行時數據區的內存如何佈局、選用哪種垃圾收集的算法、是否要對虛擬機字節碼指令進行一些內部優化操作(如使用即時編譯器把字節碼編譯爲機器碼) |
我們可以很簡單的實現虛擬機,也可以設計出性能優秀的虛擬機,在遵循虛擬機規範的前提下,設計性能優秀的虛擬機往往會得到開發者青睞。
CLass文件格式:
編譯後被Java虛擬機所執行的代碼使用了一種平臺中立(不依賴於特定硬件及操作系統的)的二進制格式來表示,並且經常(但並非絕對)以文件的形式存儲,因此這種格式被稱爲Class文件格式。Class文件格式中精確地定義了類與接口的表示形式,包括在平臺相關的目標文件格式中一些細節上的慣例,例如字節序(Byte Ordering)等。 |
JAVA實現平臺無關性的基礎是虛擬機和字節碼存儲格式,使用Java編譯器可以把Java代碼編譯爲存儲字節碼的Class文件,使用JRuby等其他語言的編譯器一樣可以把程序代碼編譯成Class文件,虛擬機並不關心Class的來源是什麼語言,只要它符合Class文件應有的結構就可以在Java虛擬機中運行。
數據類型:
與Java程序語言中的數據類型相似,Java虛擬機可以操作的數據類型可分爲兩類:原始類型(Primitive Types,也經常翻譯爲原生類型或者基本類型)和引用類型(Reference Types)。與之對應,也存在有原始值(Primitive Values)和引用值(Reference Values)兩種類型的數值可用於變量賦值、參數傳遞、方法返回和運算操作。 Java虛擬機希望更多的類型檢查放在編譯期就完成,換句話說,編譯器應當在編譯期間最大努力完成可能的類型檢查,使得虛擬機在運行期不需要進行這些操作。其中原始類型不需要通過特殊標記或者額外的標記手段來在運行期確定他們的實際數據類型,也不用和引用類型區分開。虛擬機的字節碼指令本身就可以確定他們的指令操作數的類型是什麼,利用這種特性可以直接確定操作數的數據類型。舉個列子:iadd、fadd、ladd和dadd這幾條指令都表示兩個數值相加,並返回相加的結果,單每條指令都有專屬的數據類型,他們依次對應:int、float、long、double,相關指令後面我們介紹。 Java虛擬機是直接支持對象的,這裏的對象可以指動態分配的某個類實例,也可以指某個數組的實例。虛擬機用Reference類型來表示某個對象的引用,Reference類型的值讀者可以想象成一個類似於指向對象的指針,每一個對象都存在多個指向它的引用,對象的操作、傳遞和檢查都通過引用它的Reference類型數據進行操作。 |
關於數據類型分類:
整形類型和整形取值:
byte類型:值爲8位有符號二進制補碼整數,默認值爲0,取值範圍是從-128至127(-27至27-1),包括-128和127。
short類型:值爲16位有符號二進制補碼整數,默認值爲0,取值範圍是從32768至32767(-215至215-1),包括32768和32767。
int類型:值爲32位有符號二進制補碼整數,默認值爲0,取值範圍是從2147483648至2147483647(-231至231-1),包括2147483648和2147483647。
long類型:值爲64位有符號二進制補碼整數,默認值爲0,取值範圍是從9223372036854775808至9223372036854775807(-263至263-1),包括9223372036854775808和9223372036854775807。
char類型:值爲使用16位無符號整數表示,指向基本多文本平面的Unicode值,以UTF-16編碼,默認值爲Unicode的null值,取值範圍是從0至65535,包括0和65535
浮點類型類型和取值:
float類型:值爲單精度浮點數集合中的元素,或者是(如果虛擬機支持)單精度擴展指數集合中的元素,默認值爲正整數0.
double類型:值爲雙精度浮點數集合中的元素,或者是(如果虛擬機支持)雙精度擴展指數集合中的元素,默認值爲正整數0.
java中float、double都遵循IEEE754標準,不清楚的可以看http://zangyanan.blog.51cto.com/11610700/1854836瞭解一下
布爾類型:
boolean類型:取值範圍爲true和false,Java虛擬機不提供操作boolean類型的字節碼指令,程序在編譯後boolean類型都轉化成了int操作。但是Java虛擬機支持boolean類型的數組的訪問和修改,共用byte類型數組的字節碼指令。
returnAddress類型:
returnAddress類型:returnAddress類型會被Java虛擬機的jsr、ret和jsr_w指令所使用。returnAddress類型的值指向一條虛擬機指令的操作碼。與前面介紹的那些數值類的原始類型不同,returnAddress類型在Java語言之中並不存在相應的類型,也無法在程序運行期間更改returnAddress類型的值。
運行時數據區:
Java虛擬機在執行Java的過程中會把管理的內存劃分爲若干個不同的數據區域。這些區域有各自的用途,以及創建和銷燬的時間,有的區域隨着虛擬機進程的啓動而存在,而有的區域則依賴線程的啓動和結束而創建和銷燬。如圖:
程序計數器
程序計數器是一塊較小的區域,它的作用可以看做是當前線程所執行的字節碼的行號指示器。在虛擬機的模型裏,字節碼指示器就是通過改變程序計數器的值來指定下一條需要執行的指令。分支,循環等基礎功能就是依賴程序計數器來完成的。 由於java虛擬機的多線程是通過輪流切換並分配處理器執行時間來完成,一個處理器同一時間只會執行一條線程中的指令。爲了線程恢復後能夠恢復正確的執行位置,每條線程都需要一個獨立的程序計數器,以確保線程之間互不影響。所以程序計數器是“線程私有”的內存。 如果虛擬機正在執行的是一個Java方法,則計數器指定的是字節碼指令對應的地址,如果正在執行的是一個本地方法,則計數器指定問空undefined。程序計數器區域是Java虛擬機中唯一沒有定義OutOfMemory異常的區域。 |
Java虛擬機棧
每一條Java虛擬機線程都有自己私有的Java虛擬機棧,這個棧與線程同時創建,用於存儲棧幀。Java虛擬機棧的作用與傳統語言(例如C語言)中的棧非常類似,就是用於存儲局部變量與一些過程結果的地方。另外,它在方法調用和返回中也扮演了很重要的角色。因爲除了棧幀的出棧和入棧之外,Java虛擬機棧不會再受其他因素的影響,所以棧幀可以在堆中分配,Java虛擬機棧所使用的內存不需要保證是連續的。 Java虛擬機規範允許Java虛擬機棧被實現成固定大小的或者是根據計算動態擴展和收縮的。如果採用固定大小的Java虛擬機棧設計,那每一條線程的Java虛擬機棧容量應當在線程創建的時候獨立地選定。Java虛擬機實現應當提供給程序員或者最終用戶調節虛擬機棧初始容量的手段,對於可以動態擴展和收縮Java虛擬機棧來說,則應當提供調節其最大、最小容量的手段。 Java虛擬機棧可能出現兩種類型的異常: 1. 線程請求的棧深度大於虛擬機允許的棧深度,將拋出StackOverflowError。 2.虛擬機棧空間可以動態擴展,當動態擴展是無法申請到足夠的空間時,拋出OutOfMemory異常。 棧幀: 棧幀(Frame)是用來存儲數據和部分過程結果的數據結構,同時也被用來處理動態鏈接(Dynamic Linking)、方法返回值和異常分派(Dispatch Exception)。 棧幀隨着方法調用而創建,隨着方法結束而銷燬——無論方法是正常完成還是異常完成(拋出了在方法內未被捕獲的異常)都算作方法結束。棧幀的存儲空間分配在Java虛擬機棧之中,每一個棧幀都有自己的局部變量表(Local Variables)、操作數棧(Operand Stack)和指向當前方法所屬的類的運行時常量池的引用。 局部變量表和操作數棧的容量是在編譯期確定,並通過方法的Code屬性保存及提供給棧幀使用。因此,棧幀容量的大小僅僅取決於Java虛擬機的實現和方法調用時可被分配的內存。 在一條線程之中,只有目前正在執行的那個方法的棧幀是活動的。這個棧幀就被稱爲是當前棧幀(Current Frame),這個棧幀對應的方法就被稱爲是當前方法(Current Method),定義這個方法的類就稱作當前類(Current Class)。對局部變量表和操作數棧的各種操作,通常都指的是對當前棧幀的對局部變量表和操作數棧進行的操作。 如果當前方法調用了其他方法,或者當前方法執行結束,那這個方法的棧幀就不再是當前棧幀了。當一個新的方法被調用,一個新的棧幀也會隨之而創建,並且隨着程序控制權移交到新的方法而成爲新的當前棧幀。當方法返回的之際,當前棧幀會傳回此方法的執行結果給前一個棧幀,在方法返回之後,當前棧幀就隨之被丟棄,前一個棧幀就重新成爲當前棧幀了。 請讀者特別注意,棧幀是線程本地私有的數據,不可能在一個棧幀之中引用另外一條線程的棧幀。 局部變量表: 每個棧幀內部都包含一組稱爲局部變量表(Local Variables)的變量列表。棧幀中局部變量表的長度由編譯期決定,並且存儲於類和接口的二進制表示之中,既通過方法的Code屬性保存及提供給棧幀使用。 一個局部變量可以保存一個類型爲boolean、byte、char、short、float、reference和returnAddress的數據,兩個局部變量可以保存一個類型爲long和double的數據。 局部變量使用索引來進行定位訪問,第一個局部變量的索引值爲零,局部變量的索引值是從零至小於局部變量表最大容量的所有整數。 long和double類型的數據佔用兩個連續的局部變量,這兩種類型的數據值採用兩個局部變量之中較小的索引值來定位。例如我們講一個double類型的值存儲在索引值爲n的局部變量中,實際上的意思是索引值爲n和n+1的兩個局部變量都用來存儲這個值。索引值爲n+1的局部變量是無法直接讀取的,但是可能會被寫入,不過如果進行了這種操作,就將會導致局部變量n的內容失效掉。 上文中提及的局部變量n的n值並不要求一定是偶數,Java虛擬機也不要求double和long類型數據採用64位對其的方式存放在連續的局部變量中。虛擬機實現者可以自由地選擇適當的方式,通過兩個局部變量來存儲一個double或long類型的值。 Java虛擬機使用局部變量表來完成方法調用時的參數傳遞,當一個方法被調用的時候,它的參數將會傳遞至從0開始的連續的局部變量表位置上。特別地,當一個實例方法被調用的時候,第0個局部變量一定是用來存儲被調用的實例方法所在的對象的引用(即Java語言中的“this”關鍵字)。後續的其他參數將會傳遞至從1開始的連續的局部變量表位置上。 操作數棧: 每一個棧幀內部都包含一個稱爲操作數棧(Operand Stack)的後進先出(Last-In-First-Out,LIFO)棧。棧幀中操作數棧的長度由編譯期決定,並且存儲於類和接口的二進制表示之中,既通過方法的Code屬性保存及提供給棧幀使用。 在上下文明確,不會產生誤解的前提下,我們經常把“當前棧幀的操作數棧”直接簡稱爲“操作數棧”。 操作數棧所屬的棧幀在剛剛被創建的時候,操作數棧是空的。Java虛擬機提供一些字節碼指令來從局部變量表或者對象實例的字段中複製常量或變量值到操作數棧中,也提供了一些指令用於從操作數棧取走數據、操作數據和把操作結果重新入棧。在方法調用的時候,操作數棧也用來準備調用方法的參數以及接收方法返回結果。 舉個例子,iadd字節碼指令的作用是將兩個int類型的數值相加,它要求在執行的之前操作數棧的棧頂已經存在兩個由前面其他指令放入的int型數值。在iadd指令執行時,2個int值從操作棧中出棧,相加求和,然後將求和結果重新入棧。在操作數棧中,一項運算常由多個子運算(Subcomputations)嵌套進行,一個子運算過程的結果可以被其他外圍運算所使用。 每一個操作數棧的成員(Entry)可以保存一個Java虛擬機中定義的任意數據類型的值,包括long和double類型。 在操作數棧中的數據必須被正確地操作,這裏正確操作是指對操作數棧的操作必須與操作數棧棧頂的數據類型相匹配,例如不可以入棧兩個int類型的數據,然後當作long類型去操作他們,或者入棧兩個float類型的數據,然後使用iadd指令去對它們進行求和。有一小部分Java虛擬機指令(例如dup和swap指令)可以不關注操作數的具體數據類型,把所有在運行時數據區中的數據當作裸類型(Raw Type)數據來操作,這些指令不可以用來修改數據,也不可以拆散那些原本不可拆分的數據,這些操作的正確性將會通過Class文件的校驗過程來強制保障。 在任意時刻,操作數棧都會有一個確定的棧深度,一個long或者double類型的數據會佔用兩個單位的棧深度,其他數據類型則會佔用一個單位深度。 動態鏈接: 每一個棧幀內部都包含一個指向運行時常量池的引用來支持當前方法的代碼實現動態鏈接(Dynamic Linking)。在Class文件裏面,描述一個方法調用了其他方法,或者訪問其成員變量是通過符號引用(Symbolic Reference)來表示的,動態鏈接的作用就是將這些符號引用所表示的方法轉換爲實際方法的直接引用。類加載的過程中將要解析掉尚未被解析的符號引用,並且將變量訪問轉化爲訪問這些變量的存儲結構所在的運行時內存位置的正確偏移量。 由於動態鏈接的存在,通過晚期綁定(Late Binding)使用的其他類的方法和變量在發生變化時,將不會對調用它們的方法構成影響。 方法調用結果: ⑴方法正常調用完成 方法正常調用完成是指在方法的執行過程中,沒有任何異常被拋出——包括直接從Java虛擬機之中拋出的異常以及在執行時通過throw語句顯式拋出的異常。如果當前方法調用正常完成的話,它很可能會返回一個值給調用它的方法,方法正常完成發生在一個方法執行過程中遇到了方法返回的字節碼指令的時候,使用哪種返回指令取決於方法返回值的數據類型(如果有返回值的話)。 在這種場景下,當前棧幀承擔着回覆調用者狀態的責任,其狀態包括調用者的局部變量表、操作數棧和被正確增加過來表示執行了該方法調用指令的程序計數器等。使得調用者的代碼能在被調用的方法返回並且返回值被推入調用者棧幀的操作數棧後繼續正常地執行。 ⑵方法異常調用完成 方法異常調用完成是指在方法的執行過程中,某些指令導致了Java虛擬機拋出異常,並且虛擬機拋出的異常在該方法中沒有辦法處理,或者在執行過程中遇到了athrow字節碼指令顯式地拋出異常,並且在該方法內部沒有把異常捕獲住。如果方法異常調用完成,那一定不會有方法返回值返回給它的調用者。 |
Java堆
在Java虛擬機中,堆(Heap)是可供各條線程共享的運行時內存區域,也是供所有類實例和數組對象分配內存的區域。 Java堆在虛擬機啓動的時候就被創建,它存儲了被自動內存管理系統(Automatic Storage Management System,也即是常說的“Garbage Collector(垃圾收集器)”)所管理的各種對象,這些受管理的對象無需,也無法顯式地被銷燬。本規範中所描述的Java虛擬機並未假設採用什麼具體的技術去實現自動內存管理系統。虛擬機實現者可以根據系統的實際需要來選擇自動內存管理技術。Java堆的容量可以是固定大小的,也可以隨着程序執行的需求動態擴展,並在不需要過多空間時自動收縮。Java堆所使用的內存不需要保證是連續的。 Java虛擬機實現應當提供給程序員或者最終用戶調節Java堆初始容量的手段,對於可以動態擴展和收縮Java堆來說,則應當提供調節其最大、最小容量的手段。Java堆可能發生如下異常情況:如果實際所需的堆超過了自動內存管理系統能提供的最大容量,那Java虛擬機將會拋出一個OutOfMemoryError異常。 |
拓展-----堆與棧的關係
堆和棧是程序運行的關鍵,很有必要把他們的關係說清楚。
方法區:
在Java虛擬機中,方法區(Method Area)是可供各條線程共享的運行時內存區域。方法區與傳統語言中的編譯代碼儲存區(Storage Area Of Compiled Code)或者操作系統進程的正文段(Text Segment)的作用非常類似,它存儲了每一個類的結構信息,例如運行時常量池(Runtime Constant Pool)、字段和方法數據、構造函數和普通方法的字節碼內容、還包括一些在類、實例、接口初始化時用到的特殊方法。 方法區在虛擬機啓動的時候被創建,雖然方法區是堆的邏輯組成部分,但是簡單的虛擬機實現可以選擇在這個區域不實現垃圾收集。這個版本的Java虛擬機規範也不限定實現方法區的內存位 置和編譯代碼的管理策略。方法區的容量可以是固定大小的,也可以隨着程序執行的需求動態擴展,並在不需要過多空間時自動收縮。方法區在實際內存空間中可以是不連續的。 Java虛擬機實現應當提供給程序員或者最終用戶調節方法區初始容量的手段,對於可以動態擴展和收縮方法區來說,則應當提供調節其最大、最小容量的手段。 方法區可能發生如下異常情況:
|
運行時常量池:
運行時常量池(Runtime Constant Pool)是每一個類或接口的常量池(Constant_Pool)的運行時表示形式,它包括了若干種不同的常量:從編譯期可知的數值字面量到必須運行期解析後才能獲得的方法或字段引用。運行時常量池扮演了類似傳統語言中符號表(Symbol Table)的角色,不過它存儲數據範圍比通常意義上的符號表要更爲廣泛。 每一個運行時常量池都分配在Java虛擬機的方法區之中,在類和接口被加載到虛擬機後,對應的運行時常量池就被創建出來。 在創建類和接口的運行時常量池時,可能會發生如下異常情況:
|
本地方法棧:
Java虛擬機實現可能會使用到傳統的棧(通常稱之爲“C Stacks”)來支持native方法(指使用Java以外的其他語言編寫的方法)的執行,這個棧就是本地方法棧(Native Method Stack)。當Java虛擬機使用其他語言(例如C語言)來實現指令集解釋器時,也會使用到本地方法棧。如果Java虛擬機不支持natvie方法,並且自己也不依賴傳統棧的話,可以無需支持本地方法棧,如果支持本地方法棧,那這個棧一般會在線程創建的時候按線程分配。
Java虛擬機規範允許本地方法棧被實現成固定大小的或者是根據計算動態擴展和收縮的。如果採用固定大小的本地方法棧,那每一條線程的本地方法棧容量應當在棧創建的時候獨立地選定。一般情況下,Java虛擬機實現應當提供給程序員或者最終用戶調節虛擬機棧初始容量的手段,對於長度可動態變化的本地方法棧來說,則應當提供調節其最大、最小容量的手段。 本地方法棧可能發生如下異常情況:
|