上節學習回顧java
在上一節當中,主要以本身的工做環境簡單地介紹了一下自身的一些調優或者說是故障處理經驗。所謂百變不離其宗,這個宗就是咱們解決問題的思路了。編程
本節學習重點數組
在前面幾章,咱們宏觀地瞭解了虛擬機的一些運行機制,那麼從這一章節開始,咱們將更加深刻虛擬機的深處去了解其運行細節了。例如本章節的學習重點是類文件的結構,也就是虛擬機的數據入口。既然是數據入口,確定得要符合虛擬機的數據定義規範才能給虛擬機處理,不然它壓根就不認識你。架構
概述併發
在學習以前,先拋出一個比較常見的問題:C語言與Java的運行效率如何?其實這個問題隨着技術的發展愈來愈很差回答,先看看下圖:函數
若是單單看C語言和Java語言的一個運行流程,我會毫無疑問的舉起手腳投C語言運行效率比Java的運行效率高,但隨着技術的進步和發展(後面章節會學習到的技術),我只能說Java的運行速度跟其它的高級語言相比只會愈來愈近,而且某些狀況不輸給C語言。固然,這一章節討論的不是跟其它語言比效率,是先給你和我一個比較宏觀的角度去理解類文件的位置。Java語言最大的優點就是一次編譯處處運行,不像C語言文件在不一樣的操做系統會有兼容性問題。但凡事有收穫就確定有付出的,世界上沒有那麼完美的事情,Java這跨平臺的優點也卻倒是劣勢。由於多了一層「虛擬機系統」這件「溫暖的棉襖」才得以讓Java能夠處處跑,也確實有人用C語言是裸奔而Java是裹着棉襖奔跑來形容二者的運行效率。C語言編譯後是機器語言文件能夠直接執行,而Java語言文件編譯後是類文件。類文件還須要在虛擬機運行時(解釋+編譯)轉換成機器語言才能執行。若是咱們直接去查看機器語言文件,裏面除了0就是1,這就是計算機惟一認識的兩個字。由於類文件也稱字節文件,就是以一個字節(8bit)爲單位組成的文件, 用文本打開同樣是全是0和1的二進制樣式,但類文件的二進制規則和機器語言的二進制規則又有所不一樣。例如類文件開頭的前32位(4字節)是定義類文件的標識,前32位字節若是Java虛擬機不認識,那就不是類文件了。同理,若是計算機硬件不認識這個二進制文件的排版規則,那就是這個不是機器語言。而這一章節主要學習的就是類文件是如何組成的?又有哪些規則?其實說白了,類文件也是一種語言文件,只不過面對的不是咱們這些普羅大衆的應用開發者,而是面向於那些基於Java虛擬機的語言設計者和開發者看的而已。工具
無關性的基石性能
「一次編寫,處處運行(Write Once,Run Anywhere)」是Java誕生的主要目的,這是多硬件和多操做系統時代發展的必然選擇,我想就算今天沒有JVM的誕生也會有其它跨平臺虛擬機取而代之。虛擬機充當了兼容不一樣平臺的「中間件」,不一樣平臺都會有一個對應的虛擬機,解放了字節碼文件(類文件)對不一樣平臺的兼容性(例如C語言的兼容性問題),統一了對字節碼的規範。在設計者周全的考慮下,把Java的規範拆分紅了Java語言規範《The Java Language Specification》及Java虛擬機規範《The Java Virtual Machine Specification》。也就是不僅僅只有Java語言能運行在Java虛擬機上,其它遵循Java虛擬機規範的語言同樣能夠運行在Java虛擬機上,好比JRuby、Groovy、Scala以及Clojure等語言。這些語言只要經過編譯成符合Java虛擬機規範的字節碼文件(.class)就能運行在Java虛擬機上。因爲各語言實現規範的方式不一致,因此會出現語言之間的一些特性會有所不一樣,但它們最終都是經過字節碼的命令組成的。學習
Class類文件的結構優化
Class文件是一組以8位字節爲基礎單位的二進制流,因此咱們有時候也稱之爲字節文件。各個數據項是字節按照類文件組成規範嚴格按順序緊湊地排列在Class文件之中,中間是沒有任何分隔符的,因此你們把Class文件打開來看就像看機器碼同樣一堆十六進制字符,以下圖所示:
按照Java虛擬機規範所說,Class文件格式採用一種相似了C語言結構體的僞結構來存儲數據,這種僞結構中佔有兩種數據類型:無符號數和表。無符號數就是基本的數據類型,以u一、u二、u四、u8來分表表明1個字節、2個字節、4個字節和8個字節的無符號數。而表由多個無符號數或者其它表做爲數據項構成的負荷結構數據, 全部表都習慣性地以「_info」結尾。表用於描述有層次關係的複合結構的數據,整個Class文件本質上就是一張表,它由如下數據項構成:
類型 |
名稱 |
數量 |
u4 |
magic |
1 |
u2 |
minor_version |
1 |
u2 |
major_version |
1 |
u2 |
constant_pool_count |
1 |
cp_info |
constant_pool |
constant_pool_count |
u2 |
access_flags |
1 |
u2 |
this_class |
1 |
u2 |
super_class |
1 |
u2 |
interfaces_count |
1 |
u2 |
interfaces |
interfaces_count |
u2 |
fileds_count |
1 |
filed_info |
fileds |
fileds_count |
u2 |
methods_count |
1 |
method_info |
methods |
methods_count |
u2 |
atributes_account |
1 |
atribute_info |
atributes |
atributes_account |
Class文件格式表說明
以上表格的排版結構就是整個Class文件格式的「表面結構」,也就是第一層次的結構。爲何說是「表面結構」,是由於上面也介紹過Class文件表的可擴展性的問題,每一層的表裏面還能夠隱藏着另外一個層次的表,以此類推。爲了讓人更好理解,能夠去想象一下XML的組織架構。XML看似簡單,但一樣因爲節點的擴展性問題,除了同一層次的橫向擴展,還能夠無限垂直擴展形成深度複雜的數據架構。二者道理是同樣的,不過Class的結構又不像XML等描述語言那樣,先前也提到過,因爲類文件結構沒有任何分隔符,因此在以上表格描述的數據項中不管是順心仍是數量,甚至於數據存儲的字節序這樣的細節都是被嚴格限定的,例如哪一個字節表明什麼含義,長度多少,前後順序如何等都不容許改變。而XML的同一層次的節點能夠改變順序且不影響器數據的表達。這些就像我先前說的,XML是給普羅大衆看的,而Class文件是給虛擬機看的。因此這些所謂的分隔符等人性化標誌就不須要了,省得浪費空間了。
類文件結構之:魔數與文件版本
請看如下截圖的紅色框部分,前4個字節稱爲魔數(Magic Number),它是惟一肯定這個文件爲Class文件的標識說明,許多的文件都用相似的魔數進行身份識別標識,例如GIF或JPEG等文件都存在魔數。從0XCAFEBABE(咖啡寶貝?)這個魔數大概能夠看出,爲何Java的Logo是一杯咖啡了。近接其後的就是Class文件的次版本號(第五、6個字節)和主版本號(第七、8個字節)了。從十六進制規範看是0032.0000,轉化爲十進制後就是50.00。JDK時從1.0~1.1版本使用了45.0~45.3,從JDK1.1後每一個大版本發佈主版本號向上加1,也就是46.00表示JDK1.二、47.00表示JDK1.3以此類推,那麼上文的50.00表示JDK1.6了。
類文件結構之:常量池
從Class文件的第一層結構能夠看到,magic、minor_version、major_version以後的就是constant_pool_count以及constant_pool,也就是常量池數量以及常量池,常量池數量爲u2類型,也就是佔用兩個字節,從以上的類文件能夠看到偏移量爲0X00000008日後的兩個字節就是常量池數量的值:0X0016,也就是22個常量,不過java規定常量池的索引值從1開始,第0項常量空出來是有特殊考慮,這樣作的目的在於知足後面某些指向常量池的索引值的數據在特定狀況下須要表達「不引用任何一個常量池項目」的含義。也就是說實際的常量有21個。由於「常量[0]」表示「不引用任何常量」,那麼就從常量[1]開始,而第一個常量就是緊跟隨常量數量的標識後繼續延伸,也就是類文件偏移量0X0000000A,開始翻譯類文件有哪些常量前,先介紹一下常量池的項目類型,畢竟一個常量池包含了多種類型,知道各類類型的表示方式才知道常量表示什麼,這種結構就有點相似我在「Class類文件結構」介紹的「表中表」常量池的各類項目類型就是第二層的表,以下所示:
類型 |
標誌 |
描述 |
CONSTANT_Utf8_info |
1 |
UTF-8編碼字符串 |
CONSTANT_Integer_info |
3 |
整型字面量 |
CONSTANT_Float_info |
4 |
浮點型字面量 |
CONSTANT_Long_info |
5 |
長整型字面量 |
CONSTANT_Double_info |
6 |
雙精度浮點型字面量 |
CONSTANT_Class_info |
7 |
類或接口的符號引用 |
CONSTANT_String_info |
8 |
字符串類型字面量 |
CONSTANT_Fieldref_info |
9 |
字段符號的應用 |
CONSTANT_Methodref_info |
10 |
類中方法的符號引用 |
CONSTANT_InterfaceMethodref_info |
11 |
接口中方法的符號引用 |
CONSTANT_NameAndType_info |
12 |
字段或方法的部分符號引用 |
CONSTANT_MethodHandle_info |
15 |
標識方法句柄 |
CONSTANT_MethodType_info |
16 |
標識方法類型 |
CONSTANT_InvokeDtnamic_info |
18 |
表示一個動態方法調用點 |
常量池項目類型說明
以上介紹的每一個常量項目類型都是以「_info」結尾,看來也都是表中表了。接着繼續跟蹤上文介紹的21個常量中的地1個常量,在類文件偏移量0X0000000A上的十六進制是07,再結合常量類型表的各類類型標識能夠對比出來標識7是CONSTANT_Class_info(每一個常量表的第一個字節都是常量類型標識),接下來在看看CONSTANT_Class_info的表結構:
類型 |
名稱 |
數量 |
u1 |
tag |
1 |
u2 |
name_index |
1 |
CONSTANT_Class_info型常量結構
從CONSTANT_Class_info表結構看出由u1+u2一共三個字節組成,tag表就是剛纔所說的常量標識,而接下來的兩個字節就是name_index的表示,name_index是一個索引值,既然是索引值,確定是指向其它地方去了。再看看它指向誰了。繼續下移類文件偏移量到0X0000000B來,name_index值爲0X0002,也就是指向了常量池中的第二項目(即常量[2]),從這一點能夠看出,常量項目的各類項目結構中會存在索引指向,而索引值就表明常量池中第幾個常量的意思了。那行,接下來繼續看第二個常量,由於CONSTANT_Class_info是剛纔介紹的第一個常量,那麼第二個常量就是從類文件偏移量0X0000000D處開始了,此處的值爲0X01,再對照常量項目表的標識能夠獲得此常量是一個CONSTANT_Utf8_info的類型常量,也就是一個字符串了。那行,繼續介紹CONSTANT_Utf8_info常量類型的結構:
類型 |
名稱 |
數量 |
u1 |
tag |
1 |
u2 |
length |
1 |
u1 |
bytes |
length |
CONSTANT_Utf8_info型常量結構
從CONSTANT_Utf8_info能夠看到,第一個類型(tag)爲常量標識,這都知道了。第二個類型(length)該字符串長度,也就是這個字符串有多長,那麼第三個類型表示該字符串的各個字節了,從u1也能夠看出,具體數量爲多少個字節了,也就是多少個byte呢,那得靠第二個類型字段length決定了,因此他的數量也寫着length。繼續跟蹤類文件偏移量看看length到底多少,看類文件偏移量0X0000000E,length是u2類型,佔用兩個字節,那麼值爲0X001D,十進制也就是29了,那說明這個CONSTANT_Utf8_info類型的字符串長度爲29了,那繼續把當前文件偏移量移動個29字節,字節值以下:
這串值是使用UTF-8縮略編碼表示的,UTF-8縮略編碼與普通UTF-8的區別是:從‘\u0001’到‘\u007f’之間的字符(至關於1~127的ASCII碼)的縮略編碼使用一個字節表示,從‘\u0080’到‘\u07ff’之間的全部字節的縮略編碼用兩個字節表示,從‘\u0800’到‘\uffff’之間的全部字符的縮略編碼就按照普通UTF-8編碼規則使用三個字節表示。縮略編碼說白了就是爲了節省類文件空間嗎。若是按照UTF-8縮略編碼去編譯以上29個字節,那麼轉換獲得的字符串也就是「org/fenixsoft/clazz/TestClass」,再結合第一個常量(CONSTANT_Class_info)就很明白了,這是一個完整的類名了。好了,若是再這麼解釋下去我本身都暈了,咋們仍是靠工具(javap)去看看這個類文件到底怎麼回事吧:
$javap –verbose TestClass Compiled from "TestClass.java" public class org.fenixsoft.clazz.TestClass extends java.lang.Object SourceFile: "TestClass.java" minor version: 0 major version: 50 Constant pool: const #1 = class #2; // org/fenixsoft/clazz/TestClass const #2 = Asciz org/fenixsoft/clazz/TestClass; const #3 = class #4; // java/lang/Object const #4 = Asciz java/lang/Object; const #5 = Asciz m; const #6 = Asciz I; const #7 = Asciz <init>; const #8 = Asciz ()V; const #9 = Asciz Code; const #10 = Method #3.#11; // java/lang/Object."<init>":()V const #11 = NameAndType #7:#8;// "<init>":()V const #12 = Asciz LineNumberTable; const #13 = Asciz LocalVariableTable; const #14 = Asciz this; const #15 = Asciz Lorg/fenixsoft/clazz/TestClass;; const #16 = Asciz inc; const #17 = Asciz ()I; const #18 = Field #1.#19; // org/fenixsoft/clazz/TestClass.m:I const #19 = NameAndType #5:#6;// m:I const #20 = Asciz SourceFile; const #21 = Asciz TestClass.java; { public org.fenixsoft.clazz.TestClass(); Code: Stack=1, Locals=1, Args_size=1 0: aload_0 1: invokespecial #10; //Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lorg/fenixsoft/clazz/TestClass; public int inc(); Code: Stack=2, Locals=1, Args_size=1 0: aload_0 1: getfield #18; //Field m:I 4: iconst_1 5: iadd 6: ireturn LineNumberTable: line 9: 0 LocalVariableTable: Start Length Slot Name Signature 0 7 0 this Lorg/fenixsoft/clazz/TestClass; }
從以上javap工具對類文件的解析,能夠看到Constant pool的全部內容,從內容中也能夠看到一共有21個常量,而第一個常量就是class類型的,指向了第二個常量,而第二個常量就是字符串「org/fenixsoft/clazz/TestClass」。
類文件結構之:訪問標誌
繼續按照Class第一次結構層次順序學習,接下來學習就是訪問標誌(access_flags),這些標誌經常使用於類或者接口層次的訪問信息。也很好理解,既然上面介紹了類名,接下來要說明的確定是類或者接口的訪問權限了,例如咱們寫類的第一個標識字段有public、default、protected、private,Class除了以上幾個權限標誌外,還有final的修飾等,具體標誌值請看下錶:
標誌名稱 |
標誌值 |
含義 |
ACC_PUBLIC |
0X0001 |
是否爲public類型 |
ACC_FINAL |
0X0010 |
是否被聲明爲final,只有類能夠設置 |
ACC_SUPER |
0X0020 |
是否容許使用invokespecial字節碼指令的新語意,invokespecial指令的語意在JDK1.0.2發生過改變,爲了區別這條指令使用哪一種語意,JDK1.0.2以後編譯 |
ACC_INTERFACE |
0X0200 |
標誌這是一個接口 |
ACC_ABSTRACT |
0X0400 |
是否爲abstract類型,對於接口或者抽象類來講,此標誌值爲真,其餘類爲假 |
ACC_SYNTHETIC |
0X1000 |
標誌這個類並不是由用戶代碼產生的 |
ACC_ANNOTATION |
0X2000 |
標誌這是一個註解 |
ACC_ENUM |
0X4000 |
標誌這是一個枚舉 |
類訪問標誌
從Class文件格式表說明能夠看出,訪問標誌是u2類型,也就是佔用兩個字節,咱們繼續經過類二進制文件繼續查看TestClass顯示的是什麼。把以上全部常量跳過以後來到訪問標誌的偏移地址以下所示:
0X0021也就是0X0001|0X0020的值,說明TestClass的ACC_PUBLIC和ACC_SUPER爲真。
類文件結構之:類索引、父類索引與接口索引集合
繼續從Class文件結構的第一層出發,學習完訪問標誌,接下來的三個就是類索引、父類索引與接口索引,而類索引和父類索引都是一個u2類型標識,而接口是一組u2類型的組合。咱們都知道,類引用(this)和父類引用(super)都只有一個,而接口能夠繼承多個,因此接口索引是一組組合也不難理解。繼續觀察類二進制編碼狀況:
由於是索引值,因此指向的都是常量池的內容,先看看類索引值(0X0001)指向了常量池的第一個常量,也就是類org/fenixsoft/clazz/TestClass。在看父類索引值(0X0003)指向了常量池的第三個變量,也就是java/lang/Object。而接口索引的數量值(0X0000)爲0,就是沒有接口引用。
類文件結構之:字段表集合
按照類文件結構的第一層順序看,其實跟咱們平時寫類的順序是一致的,定義類路徑、名稱、繼承關係,接下來定義的就是類屬性字段了。因此接下來要學習的就是類結構中的字段表集合標識(不包含局部變量)。從類文件結構表看到,類字段是由一個u2類型的fields_count以及field_info類型的fields組成的,每一個字段(field)都是field_info的結構,具體有多少個field那就由field_acount決定了,先來看看field_info結構如何:
類型 |
名稱 |
數量 |
u2 |
access_flags |
1 |
u2 |
name_index |
1 |
u2 |
descriptor_index |
1 |
u2 |
attribute_count |
1 |
attribute_info |
attributes |
attribute_count |
字段表結構
我相信access_flags已經很眼熟了,就是相似於Class的訪問標識,字段一樣有訪問標識,如做用域(private、prodected、 public)、static修飾符、可變性final、併發可見性volatile、能否被序列化transient、基本數據類型(基本類型、對象、數組)還有字段名稱。這些修飾信息都是布爾值,一樣相似於Class訪問標識的組成方式。下面來看看這些訪問標識的標誌值:
標誌名稱 |
標誌值 |
含義 |
ACC_PUBLIC |
0X0001 |
字段是否public |
ACC_PRIVATE |
0X0002 |
字段是否private |
ACC_PROTECTED |
0X0004 |
字段是否protected |
ACC_STATIC |
0X0008 |
字段是否static |
ACC_FINAL |
0X0010 |
字段是否final |
ACC_VOLATILE |
0X0040 |
字段是否volatile |
ACC_TRANSIENT |
0X0080 |
字段是否transient |
ACC_SYNTHETIC |
0X0100 |
字段是否由編譯器自動產生的 |
ACC_ENUM |
0X0400 |
字段是否enum |
字段訪問標誌
繼續探索field_info結構的第二個字段(name_index),經過以上學習經驗大概能夠得知以index結尾的而由沒有特殊說明的都是指向常量池的。而這個name_index就是字段的「簡單名稱」,例如private int age,age就是這個「簡單名稱」。而descriptor_index一樣是指向常量池的一個字段「描述符」,仍是以age字段爲例子,這個int就是對age的一個描述符,對於描述符,JVM還有有一套規範的:
標識字符 |
含義 |
B |
基本類型byte |
C |
基本類型char |
D |
基本類型double |
F |
基本類型float |
I |
基本類型int |
J |
基本類型long |
S |
基本類型short |
Z |
基本類型boolean |
V |
特殊類型void |
L |
對象類型。如Ljava/lang/Object |
描述符標識字符含義
除了這裏講解的「簡單名稱」和「描述符」外,還有以上類名(如org/fenixsoft/clazz/TestClass)稱爲「全限定名」。這些字符名稱還有要加以區分。
此外,字段表結構還有屬性(attribute_count和attributes),本文例子TestClass是沒有用到這個屬性的,而屬性的結構格式後續節點還有介紹,這裏暫時不詳細寫了。經過以上字段結構的屬性,再結合TestClass字節文件回顧一下以上的學習內容。下來看看TestClass有多少個字段,看TestClass字節碼錶示字段集合的地址位置:
0X0001表示的是field_count,代表TestClass類有一個字段,接下來的字節碼就是表示第一個字段結構的開始了,0X0002表示的是access_flags了,從字段訪問標誌表能夠看出該字段爲ACC_PRIVATE標識,表明私有類型。在來看第二個屬性name_index,值爲0X0005,表明指向常量池的第五個值,結合以上常量池看就是「m」了,再來看看descriptor_index值0X0006,常量池的第六個常量是I,由於這個字段的屬性數量爲0X0000,因此表明沒有屬性,因此這個字段就到此結束。不難看出,TestClass的一個字段定義爲:「private int m;」。
在這裏仍是有必要介紹一下對象類型的描述「L」,若是是一個有開發經驗的Java開發人員的話,我相信「LJava/…」這類字符串看到也很多了,這就是JVM規範定義的對象類型了。L爲對象類型,那麼具體是什麼類型,還得看跟在L後面的全限定名,這個名稱就是具體的對象名稱了,名稱後以「;」表明描述結束。數組同理,「[」符號表示數組類型,那具體是什麼數組類型,還得結合數組符號後面的對象標示符+全限定名了。例如「[Ljava/lang/String;」表示的是字符串數組,若是是二維數組,那就是兩個「[」符號了,如「[[Ljava/lang/String;」。若是用描述符描述方法時用「()」表示方法,方面有什麼參數,就填入具體的參數類型,返回類型緊跟括號後面,如描述方法「viod main(String[] args)」的描述符爲「([Ljava/lang/String;)V」,又或者描述方法「int indexOf(char[] source,int sourceOffset,int sourceCount,char[] target,int targetOffset,int targetCount,int fromIndex)」的描述符爲「([CII[CIII)I」,不難看出,括號表明方法,第一個參數爲char數組類型則爲[C,第二和第三個參數爲int類型則爲II,第四個又是char數組則未[C,最後三個都是int類則未III。有一個特別須要注意的是,基本類型和對象類型的描述確保,基本類型是一個大寫字母,而對象類型則須要「;」結束防止多個全限定名之間產生混淆,再如void test(String a,int b)的方法描述符爲(Ljava/lang/String;I)V。
類文件結構之:方法表集合
若是理解了上一節「字段表集合」學習內容的話,那麼這一節的方法表集合很是好理解了,由於表元素都是同樣的,以下:
類型 |
名稱 |
數量 |
u2 |
access_flags |
1 |
u2 |
name_index |
1 |
u2 |
descriptor_index |
1 |
u2 |
attribute_count |
1 |
attribute_info |
attributes |
attribute_count |
方法表結構
至於字段的訪問標誌和方法的訪問標誌多少仍是有些出入的,以下所示:
標誌名稱 |
標誌值 |
含義 |
ACC_PUBLIC |
0X0001 |
方法是否public |
ACC_PRIVATE |
0X0002 |
方法是否private |
ACC_PROTECTED |
0X0004 |
方法是否protected |
ACC_STATIC |
0X0008 |
方法是否static |
ACC_FINAL |
0X0010 |
方法是否final |
ACC_SYNCHRONIZED |
0X0020 |
方法是否synchronized |
ACC_BRIDGE |
0X0040 |
方法是否由編譯器產生的橋接方法 |
ACC_VARARGS |
0X0080 |
方法是否接受不定參數 |
ACC_NATIVE |
0X0100 |
方法是否爲native |
ACC_ABSTRACT |
0X0400 |
方法是否爲abstract |
ACC_STRICTFP |
0X0800 |
方法是否爲strictfp |
ACC_SYNTHETIC |
0X1000 |
防範是否由編譯器自動產生 |
方法訪問標誌
經過方法表結構結合TestClass類文件繼續追中該字節碼的奧祕,請看一下方法表結構的字節碼:
方法表結構入口在以上的0X0002值上,第一個u2類型的值表明方法的數量,經過字節碼看到TestClass類有兩個方法。這裏有個疑問,我記得寫TestClass的時候只有一個inc方法啊,如今怎麼會出現兩個呢,繼續觀察。看看第一個方法的訪問標誌爲0X0001,參考訪問標誌標識表就知道這是一個public修飾的方法,方法名索引指向了常量池的第0X0007個常量,結合常量池就知道方法名爲「<init>」,描述符的索引值爲0X0008,也就是「()V」,attributeCount值爲0X0001,代表這個方法有一個屬性,這個屬性須要特別提到的就是,方法體的代碼都是放在屬性上的,因此這個屬性幾時init方法的實現代碼。稍後一節再詳細談論屬性表結構。這個init方法是編譯器自動添加的,是類文件構造函數的入口。因此也就明白爲何會有兩個方法了。
類文件結構之:屬性表集合
終於來到了類文件表層結構的最後一個環節「屬性表結構」了,在前面的講解中已經出現屢次,在Class文件、字段表、方法表均可以攜帶本身的屬性表集合。可是,這個屬性表結構的豐富程度比以上的表結構大得多,下面一一學習吧。
與Class文件中其餘的數據項目要求嚴格的順序、長度和內容不一樣,屬性表集合的限制稍微寬鬆了一些,再也不要求各個屬性表具備嚴格順序,而且只要不與已有屬性名重複,任何人實現的編譯器均可以想屬性表中寫入本身定義的屬性信息,Java虛擬機運行時會忽略掉它不認識的屬性。也就是說,不一樣的屬性都各類有本身一套結構規則,例如上文說到的Code屬性。最新的《Java虛擬機規範(Java SE 7)》版中,屬性項已經增長到21項了。因此,屬性結構已經有21種,若是須要學習,能夠閱讀本書的6.3.7章節。本學習文字只對部分屬性進行學習和理解。
屬性名稱 |
使用位置 |
含義 |
Code |
方法表 |
Java代碼編譯成的字節碼指令 |
ConstantValue |
字段表 |
final關鍵字定義的常量值 |
Deprecated |
類、方法表、字段表 |
被聲明爲deprecated的方法和字段 |
Exceptions |
方法表 |
方法拋出的異常 |
EnclosingMethod |
類文件 |
僅當一個類爲局部類或者匿名類時才能擁有這個屬性,這個屬性用於標識這個類所在的外圍方法 |
InnerClasses |
類文件 |
內部類列表 |
LineNumberTable |
Code屬性 |
Java源碼的行號與字節碼指令的對應關係 |
LocalVariableTable |
Code屬性 |
方法的局部變量描述 |
StackMapTable |
Code屬性 |
JDK1.6中新增的屬性,供新的類型檢查驗證器(Type Checker)檢查和處理目標方法的局部變量和操做數棧所須要的類型是否匹配 |
Signature |
類、方法表、字段表 |
JDK1.5中新增的屬性,這個屬性用於支持泛型狀況下的方法簽名,在Java語言中,任何類、接口、初始化方法或成員的泛型簽名若是包含了類型變量(Type Variables)或參數化類型(Parameterized Types),則Signature屬性會爲它記錄泛型簽名信息。因爲Java的泛型採用擦除法實現,在爲了不類型信息被擦除後致使簽名混亂,須要這個屬性記錄泛型中的相關信息 |
SourceFile |
類文件 |
記錄源文件名稱 |
SourceDebugExtension |
類文件 |
JDK1.6中新增的屬性,SourceDebugExtension屬性用於存儲額外的調試信息。譬如在進行JSP文件調試時,沒法經過Java堆棧來定位JSP文件的行號,JSR-45規範爲這些非Java語言編寫,卻須要編譯成字節碼並運行在Java虛擬機中的程序提供了一個進行調試的標準機制,使用SourceDebugExtension屬性就能夠用於存儲這個標準所新加入的調試信息 |
Synthetic |
類、方法表、字段表 |
標識方法或字段爲編譯器自動生成的 |
LocalVariableTypeTable |
類 |
JDK1.5中新增的屬性,它使用特徵簽名代替描述符,是爲了引入泛型語法以後能描述泛型參數化類型而添加 |
RuntimeVisibleAnnotations |
類、方法表、字段表 |
JDK1.5新增的屬性,爲動態註解提供支持。RuntimeVisibleAnnotations屬性用於註明哪些註解是運行時(實際上運行時就是進行反射調用)可見的 |
RuntimeInvisibleAnnotations |
類、方法表、字段表 |
JDK1.5新增的屬性,與RuntimeVisibleAnnotations屬性做用恰好相反,用於指明哪些註解是運行時不可見的 |
RuntimeVisibleParameterAnnotations |
方法表 |
JDK1.5新增的屬性,做用與RuntimeVisibleAnnotations屬性相似,只不過做用對象爲方法參數 |
RuntimeInvisibleParameterAnnotations |
方法表 |
JDK1.5新增的屬性,做用與RuntimeInvisibleAnnotations屬性相似,只不過做用對象爲方法參數 |
AnnotationDefault |
方法表 |
JDK1.5新增的屬性,用於記錄註解類元素的默認值 |
BootstrapMethods |
類文件 |
JDK1.7中新增的屬性,用於保存invokedynamic指令引用的引導方法限定符 |
虛擬機規範定義的屬性
以上爲虛擬機規範(1.7以前)定義的屬性,對於每一個屬性,它的名稱須要從常量池引用一個CONSTANT_Utf8_info類型的常量來表示,而屬性值的結構則徹底自定義的,只須要經過一個u4的長度屬性去說明屬性值所佔用的位數便可。一個符號規則的屬性表應該知足如下定義結構:
類型 |
名稱 |
數量 |
u2 |
attribute_name_index |
1 |
u4 |
attribute_length |
1 |
u1 |
info |
attribute_length |
屬性表結構
屬性表集合之:Code屬性
Java程序方法體中的代碼通過Javac編譯處理後,最終變爲字節碼指令存儲在Code屬性中,但並不是全部方法表都有Code屬性,例如抽象類或接口。下面來看看Code屬性表結構:
類型 |
名稱 |
數量 |
u2 |
attribute_name_index |
1 |
u4 |
attribute_length |
1 |
u2 |
max_stack |
1 |
u2 |
max_locals |
1 |
u4 |
code_length |
1 |
u1 |
code |
code_length |
u2 |
exception_table_length |
1 |
exception_info |
exception_table |
exception_table_length |
u2 |
attribute_count |
|
attribute_info |
attributes |
attribute_count |
Code屬性表結構
attribute_name_index是一項指向常量池的索引,上文也介紹過全部屬性的的表結構必有attribute_name_index字段。而attribute_name_index所指向的utf8常量值表明這屬性的類型(可結合參考虛擬機規範定義的屬性)。例如Code屬性表的attribute_name_inde指向常量池的utf8字符串就是「Code」。attribute_length標識屬性的總長度,意味着這個屬性一共有佔多少字節。因爲attribute_name_index和attribute_length一共佔用了6個字節,那麼屬性的真實長度是整個屬性表長度-6。max_stack表明了操做數棧深度的最大值。max_locals表明了局部變量所表示的存儲空間(單位是Slot),一個Slot佔用32個字節,若是是double或long這種64位的數據類型則須要兩個Slot來存放。佔用Slot的變量包括方法參數、局部變量、異常變量等,Javac編譯器會根據變量的做用域來分配Slot,每一個Slot在整個線程週期能夠重複使用,而後根據變量數和做用域計算出max_locals的大小。code_length和code是用來存儲Java源程序編譯後產生的字節碼指令,code_length表明字節碼長度,既然叫字節碼,每一個指令確定佔用一個字節長度,因此一個字節取值範圍爲0~255,那麼字節碼指令確定不會超過255個指令,事實上目前Java虛擬機規範定義了其中約200條編碼值對應指令的含義,若是往後超過的話,擴展到雙字節的時候,有可能更名爲雙字節碼了,呵呵。由於code_length是一個u4類型,因此理論上每一個方法的字節長度不能超過2^23-1,可是虛擬機規範中明確限定了一個方法不能超過65535條字節碼指令,即實際只用到了u2的長度。有一點須要注意的是,Java虛擬機的字節碼有一個特殊情形,就是某些指令(字節碼)後面會帶有參數,因此全部code的字節碼不必定全是指令,有多是指令後的參數,下面繼續對TestClass的字節碼進行分析:
以上標藍的字節碼就是第一個方法的Code屬性表結構字節碼,0X0009表明屬性名指向常量池的值(attribute_name_index),也就是常量池的第9個常量「Code」,由於這是一個Code屬性的屬性表;而後的0X0000002F就是這個Code屬性表的屬性值的長度;0X0001說明這個堆棧的深度爲1;0X0001說明該堆棧總共有一個Slot局部存儲;0X00000005意味着該方法一共有5個字節的字節碼長度。那具體這5個字節碼分表表明什麼指令或參數呢?繼續參量Java虛擬機字節碼指令表學習:
1)第一個字節碼2A:對應指令爲aload_0,這個指令的含義是將第0個Slot中爲reference類型的本地變量推送到棧頂。
2)第二個字節碼B7:對應指令爲invokespecial,這條指令的做用是以棧頂的reference類型的數據指向的對象做爲方法接收者,調用此對象的實例構造器方法、private方法或者它的父類方法。這個方法有一個u2類型的參數說明具體調用哪一個方法,它指想常量池中的一個CONSTANT_Methodref_info類型的常量,即此方法的方法符號引用。
3)讀取invokespecial的u2類型參數:0X000A指向常量池對應的常量爲實例構造函數「<init>」方法的符號引用。第10個常量池是一個Method類型的常量,以下所示:
const #10 = Method #3.#11; // java/lang/Object."<init>":()V const #11 = NameAndType #7:#8;// "<init>":()V
它一樣是由其餘常量組成的,組成的值爲「java/lang/Object.」<init>」()V」,意思是調用Object對象的init方法,這個方法描述符是沒法無返回類型「()V」。
4)讀入B1,對應的指令爲return,含義是返回此方法,而且返回值爲void。這條指令執行後,當前方法結束。再來看一下這個方法的反編譯出來的指令描述:
public org.fenixsoft.clazz.TestClass(); Code: Stack=1, Locals=1, Args_size=1 0: aload_0 1: invokespecial #10; //Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lorg/fenixsoft/clazz/TestClass;
從以上截圖看到一個陌生的詞語「Args_size」,意思很明顯是參數個數,既然構造函數是無參的,爲何還會有一個呢?並且默認的構造函數是沒有局部變量的,又爲何會有一個局部變量呢?再往下看有一個本地變量表(LocalVariableTable),肉眼能夠看出裏面存放了一個變量,一個類型爲TestClass的this變量。有Java編程經驗的人都知道,任何對象實例均可經過this獲取對象的屬性。實際上是Javac編譯器編譯的時候把對this關鍵字的訪問轉變爲對一個普通方法參數的訪問,而後在虛擬機調用實例方法時自動傳入此參數而已。所以在實例方法的局部變量表中至少會存在一個指向當前對象實例的局部變量,局部變量也會預留出第一個Solt位來存放對象實例引用,方法參數值從1開始計算。這個處理對實例方法有效,對靜態方法就無效了。
另外,雖然在本文例子中exception_table_length爲0,但仍是有必要介紹一下exception_info的表結構:
類型 |
名稱 |
數量 |
u2 |
start_pc |
1 |
u2 |
end_pc |
1 |
u2 |
handle_pc |
1 |
u2 |
catch_type |
1 |
以上字段的意思是若是當字節碼在第start_pc行到end_pc行之間(不含第end_pc行)出現了類型爲catch_type或其子類異常(catch_type爲指向一個CONSTANT_Class_info型常量的索引),則轉到第handler_pc行繼續處理。當catch_type的值爲0時,表明任意異常狀況都須要轉向到handler_pc處進行處理。在以上的TestClass例子中沒有異常捕獲,那麼就我來重寫一下inc方法添加try-catch-finally代碼學習吧。
源碼以下:
package org.fenixsoft.clazz; public class TestClass { public int inc() { int x; try{ x = 1; return x; }catch(Exception e){ x = 2; return x; }finally{ x = 3; } } }
字節碼以下:
Code: Stack=1, Locals=5, Args_size=1 0: iconst_1 //常量值1進棧 1: istore_1 //將棧頂int型數值(1)出棧並存入第二個局部變量(Locals[1]=1) 2: iload_1 //將第二個int型局部變量(1)進棧 3: istore 4 //將棧頂int型數值(1)出棧存入第五個局部變量(Locals[4]=1) 5: iconst_3 //常量值3進棧(把3入棧) 6: istore_1 //將棧頂int型數值(3)存入第二個局部變量(Locals[1]=3) 7: iload 4 //將第五個int型局部變量(1)進棧 9: ireturn //返回方法的int元素(返回棧頂元素1) 10: astore_2 //把當前棧頂元素存入到第三個局部變量(Locals[2]=X) 11: iconst_2 //常量值2進棧 12: istore_1 //把棧頂int型數值(2)出棧並存到第二個局部變量(Locals[1]=2) 13: iload_1 //把第二個局部變量(2)入棧 14: istore 4 //把棧頂int型數值2出棧並存入第五個局部變量(Locals[4]=2) 16: iconst_3 //常量值3進棧(把3入棧) 17: istore_1 //將棧頂int型數值(3)存入第二個局部變量(Locals[1]=3) 18: iload 4 //將第五個int型局部變量(2)進棧 20: ireturn //返回方法的int元素(返回棧頂元素2) 21: astore_3 //把當前棧頂元素存入到第四個局部變量(Locals[3]=Y) 22: iconst_3 //常量值3進棧(把3入棧) 23: istore_1 //將棧頂int型數值(3)存入第二個局部變量(Locals[1]=3) 24: aload_3 //把第四個局部變量(Y)入棧 25: athrow //拋出異常 Exception table: from to target type 0 5 10 Class java/lang/Exception //第0到第5行若是拋出Exception異常則跳轉到第10行 0 5 21 any //第0到第5行若是拋出任何異常則跳轉到第21行 10 16 21 any //第10到第16行若是拋出任何異常則跳轉到第21行
以上爲改造後的TestClass的JVM指令碼,結合Code和Exception table兩個區域代碼看,意思大概是0至5行若是發生Exception異常則進入21行,若是第0至5行發生任何異常(除剛纔定義的Exception)則跳轉至21行,若是第10至16行發生任何異常則跳轉至21行。看出,21行開始都是finally的處理邏輯。但不是全部的finally處理邏輯都跳轉到21行來,而是根據Exception table表的定義來跳轉,若是沒有異常,finally的邏輯已經定義在各自的指令區域,如5至6行,16至17行。因此字節碼處理邏輯並不是好像源碼邏輯那樣經過跳轉實現的,因此可能會存在字節碼的處理邏輯跟源碼的感受會天差地別的(若是編譯器優化級別夠較高的話)。
總結
學習到這基本上能夠算是弄清楚了整個Class文件的表層結構各表的含義,剩下的都是些屬性級別的學習(參考以上「虛擬機規範定義的屬性」表),學習方式都是同樣的,若是遇到了不懂的屬性表,可經過書本自行查詢並解析。若是學習過彙編指令的同窗,面對這些字節碼指令都很是容易上手,就算沒有學習過彙編指令也沒關係,對照着字節碼指令含義表一樣也能夠看出個大概,若是熟悉了指令(200+),對之後分析源碼性能或者關鍵字特性等都很是容易上手。