《Java虛擬機規范》閱讀(一):簡介和Java虛擬機結構


前言

  說到學習jvm,其實我本人並不認為學習完以后會對目前工作有什么太大的幫助。但是為了深入了解java體系,使自己在看待問題上能夠看到更本質的部分還是必須要學習的。同時對於自己的技術也是一個深入。

  閑話少說,這個系列主要是閱讀Java虛擬機規范的一些知識點的梳理和心得,后續可能還包括經典的《深入Java虛擬機》一書的系列。

  首先提供一下《Java虛擬機規范(Java SE 7)》PDF中文版的下載,這個版本要感謝ITEYE上的幾位牛人進行的翻譯,不然只能去啃英文版的了。

  下載:

  《Java虛擬機規范(JavaSE7)》

  引用下書里的概括:

  Java SE 7版的《Java虛擬機規范》整合了自1999年《Java虛擬機規范(第二版)》發布以來Java世界所出現的技術變化。另外,還修正了第二版中許多的錯誤,以及對目前主流Java虛擬機實現來說已經過時的內容。最后還處理率了一些Java虛擬機和Java語言概念的不清晰之處。

  關於虛擬機規范:

  本規范描述的是一種抽象化的虛擬機的行為,而不是任何一種(譯者注:包括Oracle公司自己的HotSpot和JRockit虛擬機)被廣泛使用的虛擬機實現。

  所有在虛擬機規范之中沒有明確描述的實現細節,都不應成為虛擬機設計者發揮創造性的牽絆,設計者可以完全自主決定所有規范中不曾描述的虛擬機內部細節,例如:運行時數據區的內存如何布局、選用哪種垃圾收集的算法、是否要對虛擬機字節碼指令進行一些內部優化操作(如使用即時編譯器把字節碼編譯為機器碼)。

Java體系和一些基本概念

  先來看一下java平台的結構圖:

      

 

  JVM與JRE、JDK關系?

  JVM:Java Virtual Machine(Java虛擬機),負責執行符合規范的Class文件

  JRE: Java Runtime Environment (java運行環境),包含JVM和類庫

  JDK: Java  Development Kit(java開發工具包),包含JRE和開發工具包,例如javac、javah

   JVM所處的位置:

        

 

   我們通常工作中所接觸的基本是Java庫和應用以及Java核心類庫,知曉如何使用就可以了,但是歸根結底代碼都是要編譯成class文件由Java虛擬機執行的,所產生的結果或者現象都可以通過Java虛擬機的運行機制來解釋。一些相同的代碼會由於虛擬機的實現不同而產生不同結果。

開始前

  這個系列我們緊扣主題,針對Java虛擬機規范,至於Java虛擬機的一些特性,例如:平台無關性,安全性等等我不會在這個系列中討論。另外可能也不會系統的說明Jvm的運行機制,畢竟只是規范,主要描述的都是Jvm中各個體系的規則和限制

Class文件格式

  編譯后被Java虛擬機所執行的代碼使用了一種平台中立(不依賴於特定硬件及操作系統的)的二進制格式來表示,並且經常(但並非絕對)以文件的形式存儲,因此這種格式被稱為Class文件格式。Class文件格式中精確地定義了類與接口的表示形式,包括在平台相關的目標文件格式中一些細節上的慣例,例如字節序(Byte Ordering)等。

  正如概念所說,Java為了能夠實現平台無關性,制定了一套自己的二進制格式,並經常以文件的方式存儲,稱為Class文件。這樣在不同平台上,只要都安裝了Java虛擬機,那么都可以運行相同的Class文件。具體的Class文件格式將在后面的章節詳細描述。

數據類型

  與Java程序語言中的數據類型相似,Java虛擬機可以操作的數據類型可分為兩類:原始類型(Primitive Types,也經常翻譯為原生類型或者基本類型)和引用類型(Reference Types)。與之對應,也存在有原始值(Primitive Values)和引用值(Reference Values)兩種類型的數值可用於變量賦值、參數傳遞、方法返回和運算操作。

  基本類型和引用類型的具體情況見下圖:

        

 

  Java虛擬機希望更多的類型檢查放在編譯期就完成,在運行期不需要進行這些操作。其中基本類型達到了這樣的要求,在運行期間不需要對其進行類型檢查,也不用和引用類型區分開。這是通過虛擬機的字節碼指令完成的,不同類型的字節碼指令中都包含了相應的數據類型。關於字節碼指令稍后介紹。

  整形類型和整型值的取值范圍如下:

對於byte類型,取值范圍是從-128至127(-27至27-1),包括-128和127。

對於short類型,取值范圍是從−32768至32767(-215至215-1),包括−32768和32767。

對於int類型,取值范圍是從−2147483648至2147483647(-231至231-1),包括−2147483648和2147483647。

對於long類型,取值范圍是從−9223372036854775808至9223372036854775807(-263至263-1),包括−9223372036854775808和9223372036854775807。

對於char類型,取值范圍是從0至65535,包括0和65535

  浮點類型、取值集合和浮點值:

  浮點類型包含32位單精度的float類型和64位雙精度的double類型兩種,浮點數除了包括正負帶符號可數的數值,還包括了正負零、正負無窮大和一個特殊的“非數字”標識(Not-a-Number,下文用NaN表示)。NaN值用於表示某些無效的運算操作,例如除數為零等情況。

  所有Java虛擬機的實現都必須支持兩種標准的浮點數值集合:單精度浮點數集合和雙精度浮點數集合。

  returnAddress類型和值:

  returnAddress類型會被Java虛擬機的jsr、ret和jsr_w指令所使用。returnAddress類型的值指向一條虛擬機指令的操作碼。與前面介紹的那些數值類的原始類型不同,returnAddress類型在Java語言之中並不存在相應的類型,也無法在程序運行期間更改returnAddress類型的值。

  boolean類型:

  Java虛擬機不提供操作boolean類型的字節碼指令,程序在編譯后boolean類型都轉化成了int操作。但是Java虛擬機支持boolean類型的數組的訪問和修改,共用byte類型數組的字節碼指令。

運行時數據區

  Java虛擬機定義了若干種程序運行期間會使用到的運行時數據區,其中有一些會隨着虛擬機啟動而創建,隨着虛擬機退出而銷毀。另外一些則是與線程一一對應的,這些與線程對應的數據區域會隨着線程開始和結束而創建和銷毀。

    下圖是Java虛擬機的邏輯構成:

          

 

  可以看出Java虛擬機的運行時數據區包括了:方法區、Java堆、Java虛擬機棧、PC寄存器、本地方法棧。

  PC寄存器:

  每個Java虛擬機線程都有自己的PC寄存器。在某個線程被新建時,會獲得一個PC寄存器。線程當前執行的方法稱為當前方法,PC寄存器用來存放當前方法中當前執行的字節碼指令的地址,如果當前方法是本地方法(Native),那么寄存器存放undefined。

  寄存器的大小至少應該能夠存放一個returnAddress類型的數據或者與平台相關的本地指針的值。

  Java虛擬機棧:

  每個Java虛擬機線程都有自己的Java虛擬機棧。Java虛擬機棧用來存放棧幀,而棧幀主要包括了:局部變量表、操作數棧、動態鏈接。

  Java虛擬機使用局部變量表來完成方法調用時的參數傳遞。局部變量表的長度在編譯期已經決定了並存儲於類和接口的二進制表示中,一個局部變量可以保存一個類型為boolean、byte、char、short、float、reference和returnAddress的數據,兩個局部變量可以保存一個類型為long和double的數據。

  Java虛擬機提供一些字節碼指令來從局部變量表或者對象實例的字段中復制常量或變量值到操作數棧中,也提供了一些指令用於從操作數棧取走數據、操作數據和把操作結果重新入棧。在方法調用的時候,操作數棧也用來准備調用方法的參數以及接收方法返回結果。

  每個棧幀中都包含一個指向運行時常量區的引用支持當前方法的動態鏈接。在Class文件中,方法調用和訪問成員變量都是通過符號引用來表示的,動態鏈接的作用就是將符號引用轉化為實際方法的直接引用或者訪問變量的運行是內存位置的正確偏移量。

  總的來說,Java虛擬機棧是用來存放局部變量和過程結果的地方。

  Java虛擬機棧可能發生如下異常情況:

  如果線程請求分配的棧容量超過Java虛擬機棧允許的最大容量時,Java虛擬機將會拋出一個StackOverflowError異常。

  如果Java虛擬機棧可以動態擴展,並且擴展的動作已經嘗試過,但是目前無法申請到足夠的內存去完成擴展,或者在建立新的線程時沒有足夠的內存去創建對應的虛擬機棧,那Java虛擬機將會拋出一個OutOfMemoryError異常。

  Java堆:

  Java堆在虛擬機啟動的時候被創建,Java堆主要用來為類實例對象和數組分配內存。Java虛擬機規范並沒有規定對象在堆中的形式。

  Java堆可能發生如下異常情況:

  如果實際所需的堆超過了自動內存管理系統能提供的最大容量,那Java虛擬機將會拋出一個OutOfMemoryError異常。

  方法區:

  方法區在虛擬機啟動的時候被創建,它存儲了每一個類的結構信息,例如運行時常量池、字段和方法數據、構造函數和普通方法的字節碼內容、還包括一些在類、實例、接口初始化時用到的特殊方法。

  方法區可能發生如下異常情況:

  如果方法區的內存空間不能滿足內存分配請求,那Java虛擬機將拋出一個OutOfMemoryError異常

  運行時常量池:

  運行時常量池(Runtime Constant Pool)是每一個類或接口的常量池的運行時表示形式,它包括了若干種不同的常量:從編譯期可知的數值字面量到必須運行期解析后才能獲得的方法或字段引用。運行時常量池在方法區中。

  在創建類和接口的運行時常量池時,可能會發生如下異常情況:

  當創建類或接口的時候,如果構造運行時常量池所需要的內存空間超過了方法區所能提供的最大值,那Java虛擬機將會拋出一個OutOfMemoryError異常。

  本地方法棧:

  本地方法棧用於支持native方法的運行。

字節碼指令集

  Java虛擬機的指令由一個字節長度的、代表着某種特定操作含義的操作碼(Opcode)以及跟隨其后的零至多個代表此操作所需參數的操作數(Operands)所構成。

  對於大部分為與數據類型相關的字節碼指令,他們的操作碼助記符中都有特殊的字符來表明專門為哪種數據類型服務:i代表對int類型的數據操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。

  加載和存儲指令:

將一個局部變量加載到操作棧的指令包括有:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>
將一個數值從操作數棧存儲到局部變量表的指令包括有:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>
將一個常量加載到操作數棧的指令包括有:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>
擴充局部變量表的訪問索引的指令:wide

  運算指令:

加法指令:iadd、ladd、fadd、dadd
減法指令:isub、lsub、fsub、dsub
乘法指令:imul、lmul、fmul、dmul
除法指令:idiv、ldiv、fdiv、ddiv
求余指令:irem、lrem、frem、drem
取反指令:ineg、lneg、fneg、dneg
位移指令:ishl、ishr、iushr、lshl、lshr、lushr
按位或指令:ior、lor
按位與指令:iand、land
按位異或指令:ixor、lxor
局部變量自增指令:iinc
比較指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp

  類型轉換指令:

  Java虛擬機對於寬化類型轉換直接支持,並不需要指令執行,包括:

int類型到long、float或者double類型
long類型到float、double類型
float類型到double類型

  窄化類型轉換指令包括有:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。但是窄化類型轉換很可能會造成精度丟失。

  對象創建與操作指令:

創建類實例的指令:new
創建數組的指令:newarray,anewarray,multianewarray
訪問類字段(static字段,或者稱為類變量)和實例字段(非static字段,或者成為實例變量)的指令:getfield、putfield、getstatic、putstatic
把一個數組元素加載到操作數棧的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
將一個操作數棧的值儲存到數組元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore
取數組長度的指令:arraylength
檢查類實例類型的指令:instanceof、checkcast

  操作數棧管理指令:

  Java虛擬機提供了一些用於直接操作操作數棧的指令,包括:pop、pop2、dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2和swap;

  控制轉移指令:

條件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt, if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。
復合條件分支:tableswitch、lookupswitch
無條件分支:goto、goto_w、jsr、jsr_w、ret

  方法調用和返回指令:

invokevirtual指令用於調用對象的實例方法,根據對象的實際類型進行分派(虛方法分派),這也是Java語言中最常見的方法分派方式。
invokeinterface指令用於調用接口方法,它會在運行時搜索一個實現了這個接口方法的對象,找出適合的方法進行調用。
invokespecial指令用於調用一些需要特殊處理的實例方法,包括實例初始化方法(§2.9)、私有方法和父類方法。
invokestatic指令用於調用類方法(static方法)。

而方法返回指令則是根據返回值的類型區分的,包括有ireturn(當返回值是boolean、byte、char、short和int類型時使用)、lreturn、freturn、dreturn和areturn,另外還有一條return指令供聲明為void的方法、實例初始化方法、類和接口的類初始化方法使用

  拋出異常指令:

athrow

類庫

  值得一提的是Java類庫中一些類的實現如果沒有Java虛擬機的支持是無法實現的,包括:

反射,譬如在java.lang.reflect包中的各個類和java.lang.Class類
類和接口的加載和創建,最顯而易見的例子就是java.lang.ClassLoader類
類和接口的鏈接和初始化,上一點的例子也適用於這點
安全,譬如在java.security包中的各個類和java.lang.SecurityManager等其他類
多線程,譬如java.lang.Thread類
弱引用,譬如在java.lang.ref包中的各個類

  也就是說針對不同的Java虛擬機的實現,以上的類庫很可能因為Java虛擬機支持的不同而帶來差異。

 

第一章的內容大概就到這了,有一些很晦澀的知識點沒有提,可能主觀意識上還是覺得沒太大作用吧,感興趣的可以自己看書。

如有異議或者錯誤的地方請補充和指正!其實這些東西自己也看了2遍了,但是這樣一整理寫出來感覺的確是印象深刻了不少,博客生活剛開始啊!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM