1、帶着問題學習JVM_Java為什么屬於編譯型+解釋型的高級語言_Class類文件結構理解


1、Java為什么屬於編譯型+解釋型的高級語言?
JAVA編譯之后生成的類文件不能直接在對應的平台上運行,而是通過JVM來翻譯才能在對應的平台上運行,而這個翻譯大多數時候是解釋的過程,但是也會有編譯,稱之為運行時編譯,即JIT(Just In Time)。綜上所述,Java是一門編譯型+解釋型的高級語言。
備注:各種不同平台的Java虛擬機,以及所有平台都統一支持的程序存儲格式—字節碼(Byte Code)是構成平台無關性的基石。
問題:為什么越來越多的程序語言選擇了與操作系統和機器指令集無關的、平台中立的格式作為程序編譯后的存儲格式?
時至今日,商業企業和開源機構已經在Java語言之外發展出一大批運行在Java虛擬機之上的語言,如Kotlin、Clojure、Groovy、JRuby、JPython、Scala等;實現語言無關性的基礎仍然是虛擬機和字節碼存儲格式。Java虛擬機不與包括Java語言在內的任何程序語言綁定,它只與“Class文件”這種特定的二進制文件格式所關聯,Class文件中包含了Java虛擬機指令集、符號表以及若干其他輔助信息。基於安全方面的考慮,《Java虛擬機規范》中要求在Class文件必須應用許多強制性的語法和結構化約束,但圖靈完備的字節碼格式,保證了任意一門功能性語言都可以表示為一個能被Java虛擬機所接受的有效的Class文件。例如:使用Java編譯器可以把Java代碼編譯為存儲字節碼的Class文件,使用JRuby等其他語言的編譯器一樣可以把它們的源程序代碼編譯成Class文件。虛擬機絲毫不關心Class的來源是什么語。
Java語言中的各種語法、關鍵字、常量變量和運算符號的語義最終都會由多條字節碼指令組合來表達,這決定了字節碼指令所能提供的語言描述能力必須比Java語言本身更加強大才行。因此,有一些Java語言本身無法有效支持的語言特性並不代表在字節碼中也無法有效表達出來,這為其他程序語言實現一些有別於Java的語言特性提供了發揮空間。
2、 javac、java、javap命令
a.javac是用來編譯.java文件
javac -encoding utf-8 -d . ClassStructure.java
b.java 執行java.class 文件
java com/zj/weblearn/test/ClassStructure
備注:使用 java 命令運行一個.class文件,需要使用該類的全限定類名,同時需要在當前路徑下有該類的包層次文件夾。這就必須要求編譯時使用 -d 選項,否則需要手動建立包層次文件夾。
c.javap 用於幫助開發者深入了解 Java 編譯器的機制
涉及的三個參數:
-c:分解方法代碼,即顯示每個方法具體的字節碼
-public | protected | package | private:用於指定顯示哪種級別的類成員
-verbose:指定顯示更進一步的詳細信息
示列:javap -c com/zj/weblearn/test/ClassStructure 輸出信息顯示如下
javap -c -verbose com/zj/weblearn/test/ClassStructure
3、Class類文件結構:
  任何一個Class文件都對應着唯一的一個類或接口的定義信息,但是反過來說,類或接口並不一定都得定義在文件里(譬如類或接口也可以動態生成,直接送入類加載器中)。也就是說它完全不需要以磁盤文件的形式存在。class文件的是一組8位為基礎單位的二進制流。各個數據項目嚴格按照順序緊湊地排列在文件之中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內容幾乎全部是程序運行的必要數據,沒有空隙存在。當遇到需要占用8個字節以上空間的數據項時,則會按照高位在前的方式分割成若干個8個字節進行存儲。
《Java虛擬機規范》規定,Class文件格式采用一種類似於C語言結構體的偽結構來存儲數據,這種偽結構中只有兩種數據類型:“無符號數”和“表”。
a.無符號數:屬於基本數據類型。以u1、u2、u4、u8分別代表1、2、4、8個字節的無符號數,用來描述數字、索引引用、數量值或者按照utf-8編碼構成的字符串值。
b.表:是由多個無符號數或者其他表做為數據項構成的復合結構數據,以“_info”結尾。用於描述有層次關系的復合結構的數據。
無論是無符號數還是表,當需要描述同一類型但數量不定的多個數據時,經常會使用一個前置的容量計數器加若干個連續的數據項的形式,這時候稱這一系列連續的某一類型的數據為某一類型的“集合”。Class的結構不像XML等描述語言,由於它沒有任何分隔符號,所以Class文件表中數據項,無論是順序還是數量,甚至於數據存儲的字節序(Byte Ordering,Class文件中字節序為Big-Endian)這樣的細節,都是被嚴格限定的,哪個字節代表什么含義,長度是多少,先后順序如何,全部都不允許改變。
class文件表中數據項(7個)的理解:
1) 魔數:每個class文件的前四個字節稱為魔數,其四個字節存儲的是class文件的版本號。第5和第6個字節是次版本號,第7和第8個字節是主版本號。從JDK 9開始,Javac編譯器不再支持使用-source參數編譯版本號小於1.5的源碼。jdk6對應的版本號為50,每升級一個版本,版本號值加1,JDK13的版本號是57。
2) 常量池:主次版本之后是常量池入口,常量池可以理解為class文件的資源倉庫。它是class文件結構中與其他項目關聯最多的數據類型,通常也是占用Class文件空間最大的數據項目之一,另外,它還是在Class文件中第一個出現的表類型數據項目。常量池中的數量不固定,所以在常量池的入口需要放置一項u2類型的數據,代表常量池容量計數值。與Java中語言習慣不同,這個容量計數是從1而不是0開始。在Class文件格式規范制定之時,設計者將第0項常量空出來是有特殊考慮的,這樣做的目的在於,如果后面某些指向常量池的索引值的數據在特定情況下需要表達“不引用任何一個常量池項目”的含義,可以把索引值設置為0來表示。Class文件結構中只有常量池的容量計數是從1開始,對於其他集合類型,包括接口索引集合、字段表集合、方法表集合等的容量計數都與一般習慣相同,是從0開始。
  常量池主要存放兩大類常量:字面量 literal 和 符號引用 symbolic references:
  a.字面量:接近於java語言層面的常量概念。如:整型字面量、浮點型字面量、長整型字面量、雙精度浮點型字面量、字符串類型字面量
  b.符號引用:屬於編譯原理方面的概念,主要包括下面6類常量:
    .被模塊導出或者開放的包(Package)
    ·類和接口的全限定名(Fully Qualified Name)
    ·字段的名稱和描述符(Descriptor)
    ·方法的名稱和描述符
    ·方法句柄和方法類型(Method Handle、Method Type、Invoke Dynamic)
    ·動態調用點和動態常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)
  備注:常量池類型所對應的標志就是字節碼中對應的標識常量類型的數字。
  Java代碼在進行Javac編譯的時候,並不像C和C++那樣有“連接”這一步驟,而是在虛擬機加載Class文件的時候進行動態連接。也就是說,在Class文件中不會保存各個方法、字段最終在內存中的布局信息,這些字段、方法的符號引用不經過虛擬機在運行期轉換的話是無法得到真正的內存入口地址,也就無法直接被虛擬機使用的。當虛擬機做類加載時,將會從常量池獲得對應的符號引用,再在類創建時或運行時解析、翻譯到具體的內存地址之中。常量池中每一項常量都是一個表,最初常量表中共有11種結構各不相同的表結構數據,后來為了更好地支持動態語言調用,后來不斷其他動態語言相關的常量。截至JDK13,常量表中分別有17種不同類型的常量。
  由於Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量來描述名稱,所以CONSTANT_Utf8_info型常量的最大長度也就是Java中方法、字段名的最大長度。而這里的最大長度就是length的最大值,既u2類型能表達的最大值65535。所以Java程序中如果定義了超過64KB英文字符的變量或方法名,即使規則和全部字符都是合法的,也會無法編譯。

3) 訪問標識:
常量池之后的2個字節,代表訪問標識。用來識別類或者接口層次的訪問信息。包括這個class是類還是接口,是否為public或者abstract。如果是類的話,是否被聲明為final等。
access_flags中一共有16個標志位可以使用,當前只定義了其中9個,沒有使用到的標志位要求一律為零。

4) 類索引(this_class)、父類索引(super_class)、接口索引集合,class文件由這三個數據項來確定這個類的繼承關系;
a.類索引、父類索引都是一個u2類型的數據。類索引用於確定這個類的全限定名,父類索引用於確定這個類的父類的全限定名。由於java語言不允許多繼承,所以父類索引只有一個。除了java.lang.Object之外,所有的Java類都有父類,因此除了java.lang.Object外,所有Java類的父類索引都不為0。
b.接口索引集合是一組u2類型數據的集合。接口索引用來描述這個類實現了哪些接口。這些被實現的接口將按implements關鍵字(如果這個Class文件表示的是一個接口,則應當是extends關鍵字)后的接口順序從左到右排列在接口索引集合中。
類索引、父類索引和接口索引集合都按順序排列在訪問標志之后,類索引和父類索引用兩個u2類型的索引值表示,它們各自指向一個類型為CONSTANT_Class_info的類描述符常量,通過CONSTANT_Class_info類型的常量中的索引值可以找到定義在CONSTANT_Utf8_info類型的常量中的全限定名字符串。
對於接口索引集合,入口的第一項u2類型的數據為接口計數器(interfaces_count),表示索引表的容量。如果該類沒有實現任何接口,則該計數器值為0,后面接口的索引表不再占用任何字節。

5)字段表:用於描述接口或者類中聲明的變量。Java語言中的“字段”(Field)包括類級變量以及實例變量,但不包括在方法內部聲明的局部變量。
字段可以包括的修飾符有字段的作用域(public、private、protected修飾符)、是實例變量還是類變量(static修飾符)、可變性(final)、並發可見性(volatile修飾符,是否強制從主內存讀寫)、可否被序列化(transient修飾符)、字段數據類型(基本類型、對象、數組)、字段名稱。上述這些信息中,各個修飾符都是布爾值,要么有某個修飾符,要么沒有,很適合使用標志位來表示。而字段叫做什么名字、字段被定義為什么數據類型,這些都是無法固定的,只能引用常量池中的常量來描述。
類的全限定名:僅僅是把類全名中的“.”替換成了“/”而已。
簡單名稱:則就是指沒有類型和參數修飾的方法或者字段名稱,這個類中的inc()方法和m字段的簡單名稱分別就是“inc”和“m”。
描述符:描述符的作用是用來描述字段的數據類型、方法的參數列表(包括數量、類型以及順序)和返回值。基本數據類型(byte、char、double、float、int、long、short、boolean)以及代表無返回值的void類型都用一個大寫字符來表示,而對象類型則用字符L加對象的全限定名來表示。對於數組類型,每一維度將使用一個前置的“[”字符來描述。用描述符來描述方法時,按照先參數列表、后返回值的順序描述,參數列表按照參數的嚴格順序放在一組小括號“()”之內。
字段表集合中不會列出從父類或者父接口中繼承而來的字段,但有可能出現原本Java代碼之中不存在的字段,譬如在內部類中為了保持對外部類的訪問性,編譯器就會自動添加指向外部類實例的字段。另外,在Java語言中字段是無法重載的,兩個字段的數據類型、修飾符不管是否相同,都必須使用不一樣的名稱,但是對於Class文件格式來講,只要兩個字段的描述符不是完全相同,那字段重名就是合法的。

6)方法表:
方法表的結構如同字段表一樣,依次包括訪問標志(access_flags)、名稱索引(name_index)、描述符索引(descriptor_index)、屬性表集合(attributes)幾項。方法中的java代碼,經過編譯器編譯成字節碼指令后,存放在方法屬性表中一個名為code的屬性里。因為volatile關鍵字和transient關鍵字不能修飾方法,所以方法表的訪問標志中沒有了 ACC_VOLATILE標志和ACC_TRANSIENT標志。與之相對,synchronized、native、strictfp和abstract 關鍵字可以修飾方法,方法表的訪問標志中也相應地增加了ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT標志。
方法里的Java代碼,經過Javac編譯器編譯成字節碼指令之后,存放在方法屬性表集合中一個名為“Code”的屬性里面,屬性表作為Class文件格式中最具擴展性的一種數據項目。與字段表集合相對應地,如果父類方法在子類中沒有被重寫(Override),方法表集合中就不會出現來自父類的方法信息。但同樣地,有可能會出現由編譯器自動添加的方法,最常見的便是類構造器“<clinit>()”方法和實例構造器“<init>()”方法。
在Java語言中,要重載(Overload)一個方法,除了要與原方法具有相同的簡單名稱之外,還要求必須擁有一個與原方法不同的特征簽名 [2] 。特征簽名是指一個方法中各個參數在常量池中的字段符號引用的集合,也正是因為返回值不會包含在特征簽名之中,所以Java語言里面是無法僅僅依靠返回值的不同來對一個已有方法進行重載的。但是在Class文件格式之中,特征簽名的范圍明顯要更大一些,只要描述符不是完全一致的兩個方法就可以共存。也就是說,如果兩個方法有相同的名稱和特征簽名,但返回值不同,那么也是可以合法共存於同一個Class文件中的。

7)屬性表:
class文件、字段表、方法表都可以攜帶自己的屬性表集合,用於描述某些場景專有的信息。與Class文件中其他的數據項目要求嚴格的順序、長度和內容不同,屬性表集合的限制稍微寬松一些,不再要求各個屬性表具有嚴格順序,並且《Java虛擬機規范》允許只要不與已有屬性名重復,任何人實現的編譯器都可以向屬性表中寫入自己定義的屬性信息,Java虛擬機運行時會忽略掉它不認識的屬性。為了能正確解析Class文件,《Java虛擬機規范》最初只預定義了9項所有Java虛擬機實現都應當能識別的屬性,而在最新的《Java虛擬機規范》的Java SE 12版本中,預定義屬性已經增加到29項。
對於每一個屬性,它的名稱都要從常量池中引用一個CONSTANT_Utf8_info類型的常量來表示,而屬性值的結構則是完全自定義的,只需要通過一個u4的長度屬性去說明屬性值所占用的位數即可。Java程序方法體里面的代碼經過Javac編譯器處理之后,最終變為字節碼指令存儲在Code屬性內。Code屬性出現在方法表的屬性集合之中,但並非所有的方法表都必須存在這個屬性,譬如接口或者抽象類中的方法就不存在Code屬性。
7.1)Code屬性表
a.max_stack代表了操作數棧(Operand Stack)深度的最大值。在方法執行的任意時刻,操作數棧都不會超過這個深度。虛擬機運行的時候需要根據這個值來分配棧幀(Stack Frame)中的操作棧深度。
b.max_locals代表了局部變量表所需的存儲空間。在這里,max_locals的單位是變量槽(Slot),變量槽是虛擬機為局部變量分配內存所使用的最小單位。對於byte、char、float、int、short、boolean和 returnAddress等長度不超過32位的數據類型,每個局部變量占用一個變量槽,而double和long這兩種64位的數據類型則需要兩個變量槽來存放。方法參數(包括實例方法中的隱藏參數“this”)、顯式異常處理程序的參數(Exception Handler Parameter,就是try-catch語句中catch塊中所定義的異常)、方法體中定義的局部變量都需要依賴局部變量表來存放。注意,並不是在方法中用了多少個局部變量,就把這些局部變量所占變量槽數量之和作為max_locals的值,操作數棧和局部變量表直接決定一個該方法的棧幀所耗費的內存,不必要的操作數棧深度和變量槽數量會造成內存的浪費。Java虛擬機的做法是將局部變量表中的變量槽進行重用,當代碼執行超出一個局部變量的作用域時,這個局部變量所占的變量槽可以被其他局部變量所使用,Javac編譯器會根據變量的作用域來分配變量槽給各個變量使用,根據同時生存的最大局部變量數量和類型計算出max_locals的大小。
c.code_length和code用來存儲Java源程序編譯后生成的字節碼指令。code_length代表字節碼長度,code是用於存儲字節碼指令的一系列字節流。
注:字節碼指令:那顧名思義每個指令就是一個u1類型的單字節,當虛擬機讀取到code中的一個字節碼時,就可以對應找出這個字節碼代表的是什么指令,並且可以知道這條指令后面是否需要跟隨參數,以及后續的參數應當如何解析。我們知道一個u1數據類型的取值范圍為0x00~0xFF,對應十進制的0~255,也就是一共可以表達256條指令。《Java虛擬機規范》已經定義了其中約200條編碼值對應的指令含義,編碼與指令之間的對應關系可查虛擬機字節碼指令表。
如果把一個Java程序中的信息分為代碼(Code,方法體里面的Java代碼)和元數據(Metadata,包括類、字段、方法定義及其他信息)兩部分,那么在整個Class文件里,Code屬性用於描述代碼,所有的其他數據項目都用於描述元數據;
Java語言里面的潛規則:在任何實例方法里面,都可以通過“this”關鍵字訪問到此方法所屬的對象。這個訪問機制對Java程序的編寫很重要,而它的實現非常簡單,僅僅是通過在Javac編譯器編譯的時候把對this關鍵字的訪問轉變為對一個普通方法參數的訪問,然后在虛擬機調用實例方法時自動傳入此參數而已。因此在實例方法的局部變量表中至少會存在一個指向當前對象實例的局部變量,局部變量表中也會預留出第一個變量槽位來存放對象實例的引用,所以實例方法參數值從1開始計算。
7.2)Exceptions屬性
Exceptions屬性是在方法表中與Code屬性平級的一項屬性Exceptions屬性的作用是列舉出方法中可能拋出的受查異常(Checked Excepitons), 也就是方法描述時在throws關鍵字后面列舉的異常。Exceptions屬性結構中的exception_index_table是一個指向常量池中CONSTANT_Class_info型常量的索引,代表了該受查異常的類型。
7.3)LineNumberTable屬性
用於描述Java源碼行號與字節碼行號(字節碼的偏移量)之間的對應關系。LineNumberTable屬性結構中的:line_number_info表包含start_pc和line_number兩個u2類型的數據項,前者是字節碼行號,后者是Java源碼行號
7.4)LocalVariableTable及LocalVariableTypeTable屬性
a.LocalVariableTable屬性用於描述棧幀中局部變量表的變量與Java源碼中定義的變量之間的關系。LocalVariableTable屬性結構中的local_variable_info項目代表了一個棧幀與源碼中的局部變量的關聯。
local_variable_info項目結構中start_pc和length屬性分別代表了這個局部變量的生命周期開始的字節碼偏移量及其作用范圍覆蓋的長度,兩者結合起來就是這個局部變量在字節碼之中的作用域范圍;name_index和descriptor_index都是指向常量池中CONSTANT_Utf8_info型常量的索引,分別代表了局部變量的名稱以及這個局部變量的描述符;index是這個局部變量在棧幀的局部變量表中變量槽的位置,當這個變量數據類型是64位類型時(double和long),它占用的變量槽為index和index+1兩個。
b.在JDK 5引入泛型之后,LocalVariableTable屬性增加了一個“姐妹屬性”-LocalVariableTypeTable。這個新增的屬性結構與LocalVariableTable非常相似,僅僅是把記錄的字段描述符的descriptor_index替換成了字段的特征簽名(Signature)。對於非泛型類型來說,描述符和特征簽名能描述的信息是能吻合一致的,但是泛型引入之后,由於描述符中泛型的參數化類型被擦除掉 [3] ,描述符就不能准確描述泛型類型了。因此出現了LocalVariableTypeTable屬性,使用字段的特征簽名來完成泛型的描述。

7.5)SourceFile及SourceDebugExtension屬性
SourceFile屬性用於記錄生成這個Class文件的源碼文件名稱,SourceFile屬性結構中的sourcefile_index數據項是指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源碼文件的文件名。為了方便在編譯器和動態生成的Class中加入供程序員使用的自定義內容,在JDK 5時,新增了SourceDebugExtension屬性用於存儲額外的代碼調試信息。例如使用非Java語言編寫的JSP文件調試時,需要編譯成字節碼運行。Java虛擬機為程序提供了一個進行調試的標准機制,使用SourceDebugExtension屬性就可以用於
存儲這個標准所新加入的調試信息,譬如讓程序員能夠快速從異常堆棧中定位出原始JSP中出現問題的行號。SourceDebugExtension屬性結構中的debug_extension存儲的就是額外的調試信息,是一組通過變長UTF-8格式來表示的字符串。一個類中最多只允許存在一個SourceDebugExtension屬性。
7.6)ConstantValue屬性
ConstantValue屬性的作用是通知虛擬機自動為靜態變量賦值。只有被static關鍵字修飾的變量(類變量)才可以使用這項屬性。
注:類似“int x=123”和“static int x=123”這樣的變量定義在Java程序里面是非常常見的事情,但虛擬機對這兩種變量賦值的方式和時刻都有所不同。
a.對非static類型的變量(也就是實例變量)的賦值是在實例構造器<init>()方法中進行的。
b.對於類變量,則有兩種方式可以選擇:在類構造器<clinit>()方法中或者使用ConstantValue屬性。
目前Oracle公司實現的Javac編譯器的選擇是,如果同時使用final和static來修飾一個變量(按照習慣,這里稱“常量”更貼切),並且這個變量的數據類型是基本類型或者java.lang.String的話,就將會生成ConstantValue屬性來進行初始化;如果這個變量沒有被final修飾,或者並非基本類型及字符串,則將會選擇在<clinit>()方法中進行初始化。
雖然有final關鍵字才更符合“ConstantValue”的語義,但《Java虛擬機規范》中並沒有強制要求字段必須設置ACC_FINAL標志,只要求有ConstantValue屬性的字段必須設置ACC_STATIC標志而已,對final關鍵字的要求是Javac編譯器自己加入的限制。而對ConstantValue的屬性值只能限於基本類型和String這點,其實並不能算是什么限制,這是理所當然的結果。因為此屬性的屬性值只是一個常量池的索引號,由於Class文件格式的常量類型中只有與基本屬性和字符串相對應的字面量,所以就算ConstantValue屬性想支持別的類型也無能為力。
從ConstantValue屬性結構結構中可以看出ConstantValue屬性是一個定長屬性,它的attribute_length數據項值必須固定為2。constantvalue_index數據項代表了常量池中一個字面量常量的引用,根據字段類型的不同,字面量可以是CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_Integer_info和CONSTANT_String_info常量中的一種。
7.7)InnerClasses屬性
InnerClasses屬性用於記錄內部類與宿主類之間的關聯。如果一個類中定義了內部類,那編譯器將會為它以及它所包含的內部類生成InnerClasses屬性。
InnerClasses屬性結構中的number_of_classes數據項代表需要記錄多少個內部類信息,每一個內部類的信息都由一個inner_classes_info表進行描述;inner_class_info_index和outer_class_info_index都是指向常量池中CONSTANT_Class_info型常量的索引,分別代表了內部類和宿主類的符號引用;inner_name_index是指向常量池中CONSTANT_Utf8_info型常量的索引,代表這個內部類的名稱,如果是匿名內部類,這項值為0。inner_class_access_flags是內部類的訪問標志,類似於類的access_flags。
7.8)Deprecated(對...強烈反對)及Synthetic(人造的)屬性
Deprecated屬性用於表示某個類、字段或者方法,已經被程序作者定為不再推薦使用,它可以通過代碼中使用“@deprecated”注解進行設置。Synthetic屬性代表此字段或者方法並不是由Java源碼直接產生的,而是由編譯器自行添加的,在JDK 5之后,標識一個類、字段或者方法是編譯器自動產生的,也可以設置它們訪問標志中的ACC_SYNTHETIC標志位。編譯器通過生成一些在源代碼中不存在的Synthetic方法、字段甚至是整個類的方式,實現了越權訪問(越過private修飾器)或其他繞開了語言限制的功能,這可以算是一種早期優化的技巧,其中最典型的例子就是枚舉類中自動生成的枚舉元素數組和嵌套類的橋接方法(Bridge Method)。所有由不屬於用戶代碼產生的類、方法及字段都應當至少設置Synthetic屬性或者ACC_SYNTHETIC標志位中的一項,唯一的例外是實例構造器“<init>()”方法和類構造器“<clinit>()”方法。
7.9)StackMapTable屬性
StackMapTable屬性在JDK 6增加到Class文件規范之中,它是一個相當復雜的變長屬性,位於Code屬性的屬性表中。這個屬性會在虛擬機類加載的字節碼驗證階段被新類型檢查驗證器(Type Checker)使用,目的在於代替以前比較消耗性能的基於數據流分析的類型推導驗證器。新的驗證器在同樣能保證Class文件合法性的前提下,省略了在運行期通過數據流分析去確認字節碼的行為邏輯合法性的步驟,而在編譯階段將一系列的驗證類型(Verification Type)直接記錄在Class文件之中,通過檢查這些驗證類型代替了類型推導過程,從而大幅提升了字節碼驗證的性能。這個驗證器在JDK 6中首次提供,並在JDK 7中強制代替原本基於類型推斷的字節碼驗證器。
StackMapTable屬性中包含零至多個棧映射幀(Stack Map Frame),每個棧映射幀都顯式或隱式地代表了一個字節碼偏移量,用於表示執行到該字節碼時局部變量表和操作數棧的驗證類型。類型檢查驗證器會通過檢查目標方法的局部變量和操作數棧所需要的類型來確定一段字節碼指令是否符合邏輯約束。一個方法的Code屬性最多只能有一個StackMapTable屬性,否則將拋出ClassFormatError異常。

7.10)Signature屬性
Signature屬性在JDK 5增加到Class文件規范之中,它是一個可選的定長屬性,可以出現於類、字段表和方法表結構的屬性表中。在JDK 5里面大幅增強了Java語言的語法,在此之后,任何類、接口、初始化方法或成員的泛型簽名如果包含了類型變量(Type Variable)或參數化類型(Parameterized Type),則Signature屬性會為它記錄泛型簽名信息。之所以要專門使用這樣一個屬性去記錄泛型類型,是因為Java語言的泛型采用的是擦除法實現的偽泛型,字節碼(Code屬性)中所有的泛型信息編譯(類型變量、參數化類型)在編譯之后都通通被擦除掉。使用擦除法的好處是實現簡單(主要修改Javac編譯器,虛擬機內部只做了很少的改動)、非常容易實現Backport,運行期也能夠節省一些類型所占的內存空間。但壞處是運行期就無法像C#等有真泛型支持的語言那樣,將泛型類型與用戶定義的普通類型同等對待,例如運行期做反射時無法獲得泛型信息。Signature屬性就是為了彌補這個缺陷而增設的,現在Java的反射API能夠獲取的泛型類型,最終的數據來源也是這個屬性。
7.11)BootstrapMethods屬性
BootstrapMethods屬性是在JDK 7時增加到Class文件規范之中,它是一個復雜的變長屬性,位於類文件的屬性表中。這個屬性用於保存invokedynamic指令引用的引導方法限定符。
7.12)MethodParameters屬性:
MethodParameters屬性是在JDK 8時新加入到Class文件格式中的,它是一個用在方法表中的變長屬性。MethodParameters的作用是記錄方法的各個形參名稱和信息。該屬性使得編譯器可以(編譯時加上-parameters參數)將方法名稱也寫進Class文件中,而且MethodParameters是方法表的屬性,與Code屬性平級的,可以運行時通過反射API獲取。
7.13)模塊化相關屬性
JDK 9的一個重量級功能是Java的模塊化功能,因為模塊描述文件(module-info.java)最終是要編譯成一個獨立的Class文件來存儲的,所以,Class文件格式也擴展了Module、ModulePackages和 ModuleMainClass三個屬性用於支持Java模塊化相關功能。
7.14)運行時注解相關屬性
早在JDK 5時期,Java語言的語法進行了多項增強,其中之一是提供了對注解(Annotation)的支持。為了存儲源碼中注解信息,Class文件同步增加了RuntimeVisibleAnnotations、RuntimeInvisibleAnnotations、RuntimeVisibleParameterAnnotations和RuntimeInvisibleParameter-Annotations四個屬性。到了JDK 8時期,進一步加強了Java語言的注解使用范圍,又新增類型注解),所以Class文件中也同步增加了RuntimeVisibleTypeAnnotations和
RuntimeInvisibleTypeAnnotations兩個屬性。
運行時注解相關屬性記錄了類、字段或方法的聲明上記錄運行時可見注解,當我們使用反射API來獲取類、字段或方法上的注解時,返回值就是通過這個屬性來取到的。



參看資料:
https://blog.csdn.net/zhouxukun123/article/details/79121059
書籍:深入理解Java虛擬機:JVM高級特性與最佳實踐第3版


免責聲明!

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



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