### **Class文件詳解:**
* 什么是Class文件?
* 如何生成一個Class文件
* Class文件的作用
* Class文件格式詳解
#### **什么是Class文件?**
* * * * *
Class文件:能夠被JVM識別,加載并執行的文件格式。
*.java 文件是人編寫的,給人看的。
*.class 是通過工具處理*.java 文件后的產物,它是給VM 看的,給VM 操作的
* * * * *
1. 就只是一種文件格式,類似于txt、mp4、mp3、word、pdf等
2. 不是只有Java語言才可以生成class文件。如下圖

:-: 圖1 class文件
3. 并不是只有Java語言才可以運行在JVM上。
①早在Java發展之初,就有設計者考慮過并實現了讓其他語言運行在Java虛擬機之上(JVM),而且發布規范文檔時,分開劃分為Java語言規范和Java虛擬機規范。在1997年發布的第一版Java虛擬機規范就曾經承諾過:“未來,我們會對Java虛擬機進行適當的擴展,以便更好地支持其他語言運行在JVM之上,”當Java虛擬機發展到JDK1.7-1.8的時候,JVM便通過JSR-292基本兌現了這個承諾。
②或許大部分程序員都認為Java虛擬機執行Java程序是一件理所當然和天經地義的事,但時至今日,商業機構和開源機構已經在Java語言之外發展出一大批在Java虛擬機之上運行的語言,如Clojure、Groovy、JRuby、Jython、Scale等。
③實現語言無關性的基礎仍然是虛擬機和字節碼(ByteCode)存儲格式。**JVM不和包括Java在內的任何語言綁定,它只和“Class文件”這種特定的二進制文件格式所關聯**,**Class文件中包含了JVM指令集和符號表達式以及若干其他輔助信息**。基于安全方面考慮,JVM規范要求在Class文件中使用許多強制性的語法和結構化約束,但任一門功能性語言都可以表示為一個能被JVM所接受的有效的Class文件。**作為一個通用的、機器無關的執行平臺,任何其他語言的實現者都可以將JVM作為語言的產品交付媒介**。比如:使用Java編譯器可以把Java代碼編譯為存儲字節碼的Class文件,使用JRuby等其它語言的編譯器一樣可以把程序代碼編譯成Class文件,**Java之所以能夠跨平臺運行,是因為Java虛擬機可以載入和執行同一種平臺無關的字節碼**。也就是說,實現語言平臺無關性的基礎是虛擬機和字節碼存儲格式,**虛擬機并不關心Class的來源是什么語言,只要它符合Class文件應有的結構就可以在Java虛擬機中運行**。

:-: 圖2 JVM提供的語言無關性
4. JAVA語言中的各種變量、關鍵字和運算符號的語義最終都是由許多條字節碼命令組合而成的,因此字節碼命令所能提供的語義描述能力肯定會比Java語言本身更強大。所以,有一些java語言本身無法持有的語言特性不代表字節碼本身無法有效持有。
#### **如何生成一個Class文件?**
1. 通過IDE自動幫我們build
2. 通過命令行手動通過javac去生成Class文件
3. 通過java命令去執行Class文件
#### **Class文件的作用**
1. 記錄一個類文件的所有信息,所有信息!!!正是因為這個原因,可以解釋為什么一個類中沒有定義this,super這些關鍵字,但是卻可以使用這些關鍵字調用父類的方法和當前的變量。
2. Class文件所包含的信息遠遠大于Java源代碼中我們所看到的信息。
#### **Class文件結構**
1. 一種8位字節的二進制流。
2. 各個數據按順序緊密的排列,無間隙。好處:減少Class文件的體積,JVM加載Class文件時更加快速
3. 每個類或者接口都單獨占據一個class文件。好處:每個類或者接口都獨自關聯自己的內容,而無需交叉
一個Class文件中只能包含一個類或者接口。**注意:任何一個Class文件都對應著唯一一個類或者接口的定義信息,但反過來說,類或者接口不一定都得定義在文件里(譬如類或者接口也可以通過類加載器直接生成)**。
關于Class文件結構的詳解,可參考[class類文件的結構](http://www.hmoore.net/alex_wsc/java/466923)
Class文件是一組以8位字節為基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在Class文件之中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內容幾乎全部都是程序運行的必要數據,沒有空隙存在。當遇到需要占用8位字節以上的空間的數據項時,則會按照高位在前的方式分割成若干個8位字節進行存儲。
根據Java虛擬機規范的規定,Class文件格式采用一種類似于C語言結構體的偽結構來存儲,**這種偽結構中只有兩種數據類型:無符號數和表。**
- **無符號數**
無符號數屬于基本的數據類型,以u1、u2、u4、u8來分別代表1個字節、2個字節、4個字節、8個字節的無符號數,無符號數可以用來描述數字、索引引用、數量值,或者按照UTF-8編碼構成字符串值。
- **表**
是由多個無符號數或者其它表作為數據項構成的復合數據類型,所有表都習慣性地以"_info"結尾。表用于描述有層次關系的復合結構的數據,整個Class文件本質上就是一張表。 它由下表所示的數據項構成。
下圖是Class文件格式([大圖這里](https://box.kancloud.cn/c0e92d9c6d5781819b8cd289d073943e_600x513.jpg))

:-: 圖3 Class文件格式
下圖是Class文件字節碼結構組織示意圖([大圖這里](https://box.kancloud.cn/04d43f4eb1df1b8a27378eae66b81a6a_849x993.png))

:-: 圖4 Class類文件字節碼結構組織示意圖
* **magic: 魔數**
所有的由Java編譯器編譯而成的class文件的前4個字節都是“0xCAFEBABE” 。
作用:當JVM在嘗試加載某個文件到內存中來的時候,會首先判斷此class文件有沒有JVM認為可以接受的“簽名”,即JVM會首先讀取文件的前4個字節,判斷該4個字節是否是“0xCAFEBABE”,如果是,則JVM會認為可以將此文件當作class文件來加載并使用。**唯一作用是確定這個文件是否是一個能被虛擬機接受的Class文件**。很多文件存儲標準都是用魔數來進行身份識別,譬如圖片格式,如gif、jpeg等,**之所以使用魔數而不是文件擴展名來進行識別主要是基于安全方面的考慮,因為文件擴展名可以隨意地改動。**
它其實就是一個加密段,類似于MD5加密一樣,給JVM來判斷當前的Class文件是否被篡改過?
- **minor_version**:Class文件的次版本號或者叫副版本號
緊接著魔數的4個字節為Class文件的版本號。第五,六個字節為次版本號,第七,八個字節為主版本號。java的主版本從45開始。**高版本的JDK可以兼容低版本的Class文件,但不能運行以后版本的class文件**,即使文件格式并未發生變化。
對于Java虛擬機來說,版本號確定了特定的Class文件格式,**通常只有給定主版本號和一系列次版本號后,Java虛擬機才能夠讀取Class文件。**
Class文件的主、次版本號是由JDK決定的,JDK1.0~JDK1.1使用了45.0~45.3的版本號(45是主版本號,點”.“之后的是次版本號),從JDK1.1開始,每個大版本的JDK主版本號加1。JDK1.1能支持版本號為45.0~45.65535的Class文件,無法執行版本號為46.0以上的Class文件,而JDK1.2則能支持45.0~46.65535的Class文件。若現在使用的是JDK1.7編譯出來的class文件,則相應的主版本號應該是51,對應的7,8個字節的十六進制的值應該是 0x33。
- **major_version** : Class文件的主版本號
同上
- **constant_pool_count**:常量池計數器
常量池是class文件中非常重要的結構,它描述著整個class文件的字面量信息。
常量池是由一組constant_pool結構體數組組成的,而數組的大小則由常量池計數器指定。
常量池計數器constant_pool_count 的值 =constant_pool表中的成員數+ 1。constant_pool表的索引值只有在大于 0 且小于constant_pool_count時才會被認為是有效的。
- **constant_pool** : 常量池數據區
一、**Java 源碼中的類名,方法名,變量名,都是以字符串形式存儲在常量池中** 。常量池包含了與文件中類和接口相關的常量。常量池中存儲了諸如文字字符串、final變量值、類名和方法名的常量。Java虛擬機把常量池組織為入口列表的形式。在實際列表constant_pool之前,是入口在列表中的計數constant_pool_count。
二、constant_pool是一種表結構,它**包含 Class 文件結構及其子結構中引用的所有字符串常量、 類或接口名、字段名和其它常量**。 常量池中的每一項都具備相同的格式特征——第一個字節作為類型標記用于識別該項是哪種類型的常量,稱為 “tag byte” 。常量池的索引范圍是 1 至constant_pool_count?1。
緊接著主次版本號之后的是常量池入口。常量池可以理解為Class文件之中的資源倉庫。它是class文件結構中與其他項目關聯最多的數據類型。,也是占用class文件空間最大的數據項目之一。同時它還是在class文件中第一個出現的表類型數據項目。
由于常量池中常量的數據是不固定的,所以在常量池的入口需要放置一薦u2類型的數據,代表常量池容量計算值(constant_pool_count)。與Java語言習慣不一樣的是,這個容量計數是從1而不是0開始的。將第0項常量出來的目的是為了滿足后面某些指向常量池的索引值的數據在特定情況下需要表達“不引用任何一個常量池項目”的意思。class文件結構中只有常量池的容量計數是從1開始,對于其它集合類型,包括接口索引集合,字段表集合,方法表集合的容量計算都是從0開始的。
三、class常量池用來存儲在“編譯期間”產生的字面量和符號引用;常量池中主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic References)。字面量比較接近于Java語言層面的常量概念,如文本字符串,被聲明為final的常量值等。而符號引用則屬性編譯原理方面的概念,包含了下面三類常量:
a.類和接口的全限定名(Fully Qualified Name)
b.字段的名稱和描述符(Descriptor)
c.方法的名稱和描述符
四、常量池中的每一項常量都是一個表,共有11種結構各不相同的表結構數據,JDK1.7中為了更好地支持動態語言調用,又額外增加了3種(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info、CONSTANT_InvokeDynamic_info),這14種表都有一個共同的特點,就是表開始的第一位是一個u1類型的標志位(tag),代表當前這個常量屬性哪種常量類型,14種常量類型具體含義如下:
五、常量池的項目類型如下表所示
表一 常量池的項目類型
| 入 口 類 型 | 標 志 值 | 描 述 |
| --- | --- | --- |
| CONSTANT_Utf8_info | 1 | UTF-8編碼的Unicode字符串 |
| CONSTANT_Integer_info |3 | int類型字面值 |
| CONSTANT_Float_info | 4 | float類型字面值 |
| CONSTANT_Long_info | 5 | long類型字面值 |
| CONSTANT_Double_info | 6 | double類型字面值 |
| CONSTANT_Class_info | 7 | 對一個類或接口的符號引用 |
| CONSTANT_String_info | 8 | String類型字面值 |
| CONSTANT_Fieldref_info | 9 | 對一個字段的符號引用 |
|CONSTANT_Methodref_info | 10 | 對一個類中聲明的方法的符號引用 |
| CONSTANT_InrerfaceMethodref_info | 11 | 對一個接口中聲明的方法的符號引用 |
| CONSTANT_NameAndType_info | 12 | 對一個字段或方法的部分符號引用 |
| CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
| CONSTANT_MethodType_info | 16 | 表示方法類型 |
| CONSTANT_InvokeDynamic_info | 18 | 表示一個動態方法調用點 |
- 每個常量池入口都從一個長度為一個字節的標志開始,這個標志指出了列表中該位置的常量類型。
每一個標志都有一個相對應的表,表名通過在標志名后加上“_info”后綴來產生。
- 之所以說常量池是最繁瑣的數據,是因為這14種常量類型各自均有自己的結構
- CONSTANT_Class_info常量的結構
表二 CONSTANT_Class_info常量的結構
| 類型 |名稱 | 數量 |
| --- | --- | --- |
| u1 | tag | 1 |
| u2 | name_index | 1 |
tag是標志位,用于區分常量類型
name_index是一個索引值,它指向常量池中一個CONSTANT_Utf8_info類型常量,該敞亮代表了這個類(或者接口)的全限定名
- CONSTANT_Utf8_info型常量的結構
表三 CONSTANT_Utf8_info型常量的結構
| 類型 |名稱 | 數量 |
| --- | --- | --- |
| u1 | tag | 1 |
| u2 | length | 1 |
| u1 | bytes | length |
length說明這個UTF-8編碼的字符串長度是多少字節。后面的bytes表示長度為length字節的連續數據是一個使用UTF-8縮略編碼表示的字符串。UTF-8縮略編碼和UTF-8編碼的區別是:從‘\u0001’到 ‘\u007f’之間的字符(相當于1~127的ASCII碼)的縮略編碼使用一個字節表示,從‘\u0080’到 ‘\u07ff’之間的字符的縮略編碼用2個字節表示,從‘\u0800’到 ‘\uffff’之間的所有字符的縮略編碼就按照普通UTF-8編碼規則使用3個字節表示。
另外,由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_inf型常量來描述名稱,所以CONSTANT_Utf8_inf型常量的最大長度也就是java中方法、字段名的最大長度。這里的最大長度就是length的最大值,既U2類型能表達的最大值65535.所以java程序中如果定義了超過64KB英文字符的變量或方法,將會無法編譯。
各常量項結構:常量池中前11種據類型的結構總表([大圖這里](https://box.kancloud.cn/520e493821970c76b709b58203d827e3_698x524.png))

:-: 圖 5 常量池中11中數據類型的結構總表.png([大圖這里](https://box.kancloud.cn/5f40d13db06e9c0b9239a7b0aa0851c1_714x409.png))

:-: 圖 6 常量池數據類型結構總表2
示例:假如我們得到的Class文件的十六進制數的一段序列為:

第9位的值為16轉換為十進制為22,代表常量池中有21個常量。第10位的07代表的是一個常量的tag值,可以從上表中看到,07代表CONSTANT_Class_info類型,從上表中可以看出,CONSTANT_Class_info類型的結構有一個u1類型的tag,有一個u2類型的name_index,數量都是1。那么可以看出接下來的第11位與第12位的值00和02就是name_index的值。即指向的常量池中的第一個常量。第二項常量的標志位為0x01(看第13位),也就是CONSTANT_Utf8_info類型,CONSTANT_Utf8_info類型有u2型的length與u1型的bytes。依此類推。
如上所述,虛擬機加載Class文件的時候,就是這樣從常量池中得到相對應的數值。
- **access_flags** : 訪問標志
在常量池結束后(0x00000a0行74(t)處結束),緊接著的兩個字節(共16位)代表訪問標志,這個標志用于標識一些類或者接口層次的訪問信息。包括這個Class是類是接口;是否定義為public類型;是否定義為abstract類型;如果是類的話是否被聲明為final等。具體含義見下表:
[大圖這里](https://box.kancloud.cn/da649f0f1b404c77e361d07d9fce8530_699x629.png)

:-: 圖7 訪問標志及注意事項
[大圖這里](https://box.kancloud.cn/973a47b33cc7cadb1d18f4740d398112_869x663.png)

:-: 圖8 訪問標志位示意圖
- **this_class** : 本類索引
類索引用來確定這個類的全限定名
類索引的作用,就是為了指出class文件所描述的這個類叫什么名字。
類索引緊接著訪問標志的后面,占有兩個字節,在這兩個字節中存儲的值是一個指向常量池的一個索引,該索引指向的是CONSTANT_Class_info常量池項,
[大圖這里](https://box.kancloud.cn/e344fcaa8c69d120dc8d497ed9263afd_791x411.png)

:-: 圖9 類索引
- **super_class** : 父類索引
父類索引用來確定父類的權限定名。
class文件中緊接著類索引(this_class)之后的兩個字節區域表示父類索引,跟類索引一樣,父類索引這兩個字節中的值指向了常量池中的某個常量池項CONSTANT_Class_info,表示該class表示的類是繼承自哪一個類。
[大圖這里](https://box.kancloud.cn/4a97993358e3c8575b51bace11d2046a_738x623.png)

:-: 圖10 父類索引
- **interfaces** : 接口索引
接口索引用來確定接口的全限定名。
一個類可以不實現任何接口,也可以實現很多個接口,為了表示當前類實現的接口信息,class文件使用了如下結構體描述某個類的接口實現信息:
[大圖這里](https://box.kancloud.cn/5037626405e313de209ed31eddce2813_830x279.png)

:-: 圖11 接口索引
- 由于類實現的接口數目不確定,所以接口索引集合的描述的前部分叫做接口計數器(interfaces_count),接口計數器占用兩個字節,其中的值表示著這個類實現了多少個接口,緊跟著接口計數器的部分就是接口索引部分了,每一個接口索引占有兩個字節,接口計數器的值代表著后面跟著的接口索引的個數。接口索引和類索引和父類索引一樣,其內的值存儲的是指向了常量池中的常量池項的索引,表示著這個接口的完全限定名。
- 接口計數器(interfaces_count)記錄的是直接繼承的接口,不包括間接繼承的接口(比如父類繼承的接口不會記錄)
類索引和父類索引都是一個兩字節的數據,而接口索引確實一組兩字節的數據,因為接口是可以多實現的。Class文件中于這三個數據來確定這個類的繼承關系。
**舉例**:
定義一個Worker接口,然后類Programmer實現這個Worker接口,然后我們觀察Programmer的接口索引集合是怎樣表示的。
~~~
/**
* Worker 接口類
* @author luan louis
*/
public interface Worker{
public void work();
}
~~~
~~~
package com.louis.jvm;
public class Programmer implements Worker {
@Override
public void work() {
System.out.println("I'm Programmer,Just coding....");
}
}
~~~
[大圖這里](https://box.kancloud.cn/3e93de273128285fd55c81a11949dcef_666x1018.png)

:-: 圖12 索引
- **field_info** : 字段表
field_info字段表用于描述接口或者類中聲明的變量,field字段包括了類級變量(靜態變量)和實例級變量(成員變量),但不包括方法內部的局部變量。
fields_count字段數目表示Class文件中的類和實例變量總數,字段存放的信息包括:字段訪問標志、是否靜態、是否final、是否并發可見volatile、是否可序列化transient、數據類型、字段名稱等等。這些信息中,各個修飾符都是布爾值,要么有某個修飾符,要么沒有,很適合使用標志位來表示。而字段叫什么名字、字段被定義為什么數據類型,這些都是無法固定的,只能引用常量池中的常量來描述。
注意:字段表中不包含從父類或者接口中繼承而來的字段,但是會添加原本代碼中不存在的字段,例如this,以及內部類對外部類訪問而自動添加的外部類實例字段等。

:-: 圖13 字段表結構
字段修飾符放在access_flags項目中,它與類中的access_flags項目是非常相似的,都是一個u2的數據類型,其中可以設置 的標志位和含義如下表所示

:-: 圖14 字段訪問標志
跟隨access_flags標志的是兩項索引值:name_index和descriptor_index。它們都是對常量池的引用,分別代表著字段的簡單名稱及字段和方法的描述符。描述符的作用是用來描述字段的數據類型、方法的參數列表(包括數量、類型以及順序)和返回值。根據描述符規則,基本數據類型(byte、char、double、float、int、long、short、boolean)及代表無返回值的void類型都用一個大寫字符來表示,而對象類型則用字符L加對象的全限定名來表示。

:-: 圖15 描述符表示字符含義
- **method_info** : 方發表
method_info方法表用于描述類或者接口中聲明的方法,methods_count用于表示Class文件中方法總數,method方法存儲了方法的訪問標識、是否靜態、是否final、是否同步synchronized、是否本地方法native、是否抽象方法abstract、方法返回值類型、方法名稱、方法參數列表等信息。
方法的代碼指令并沒有直接存放在方法表中,而是存放著屬性表中的方法表Code中。
注意:如果父類的方法在子類沒有被重寫,方法表中不會出現來自父類的方法信息,但是編譯器會自動添加類構造器”`<clinit>`”方法和實例構造器”`<init>`”方法。
Java編譯器的方法特征簽名只包括:方法名稱、參數順序和參數類型,不包括方法返回值類型,因此java的方法重載不能通過方法的返回值類區別,但是在Class文件中,方法特征簽名包括方法的返回值類型,因此Class文件中可以共存兩個名稱和參數完全相同而返回值類型不同的方法。
其結構與fields一樣,不一樣的是訪問標志。

:-: 圖16 方法訪問標志
- **attribute_info** : 屬性表
在Class文件,字段表,方法表中都可以攜帶自己的屬性表集合,以用于描述某種場景專有的信息。在屬性表的實現中,只要不與已有屬性名重復,可以在編譯時添加任何自定義的屬性信息,jvm會自動忽略掉它不認識的屬性。下表為虛擬機規范預定義的屬性:

:-: 圖17 屬性表
**示例**:
代碼
~~~
public class TestMain {
public int mX = 0;
public static void main(String[] args) {
TestMain testMainObject = new TestMain();
testMainObject.test();
System.out.println("Hello World!");
}
public TestMain() {
}
public void test() {
return;
}
}
~~~
Jvm 規范很聰明,它通過一個C 的數據結構表達了class 文件結構。這個數據結構如下圖所示:
[大圖這里](https://box.kancloud.cn/76c6e762694d497fee5ebf6dec5707fc_1030x726.jpg)

:-: 圖18 數據結構表示的Class文件內部結構
使用javap -verbose命令解析TestMain.class后的結果如下圖所示:
**注意:TestMain.class文件在F盤根目錄下**

:-: 圖19 TestMain.class解析后的結果
**常量池介紹**
注意,count_pool_count 是常量池數組長度+1。比如,假設某個Class文件常量池只有4 個元素,則count_pool_count=5)。
* * * * *
javap 解析class 文件的時候,常量池的索引從1 算起,0 默認是給VM 自己用得,一般不顯示0 這一項。這也是為什么上圖中常量池第一個元素以#1 開頭。所以,如果count_pool_count=5 的話,真正有用的元素是從count_pool[1]到count_pool[4]。
* * * * *
常量池數組的元素類型由下面的代碼表示
~~~
cp_info { //特別注意,這是介紹的cp_info 是相關元素類型的通用表達。
u1 tag; //tag 為1 個字節長。不論cp_info 具體是哪種,第一個字節一定代表tag
u1 info[]; //其他信息,長度隨tag 不同而不同
}
//tag 取值,先列幾個簡單的:
tag=7 <==info 代表這個cp_info 是CONSTANT_Class_info 結構體
tag=9 <==info 代表CONSTANT_Fieldref_info 結構體
tag=10 <==info 代表CONSTANT_Methodref_info 結構體
tag=8 <==info 代表CONSTANT_String_info 結構體
tag=1 <==info 代表CONSTANT_Utf8_info 結構體
~~~
在JVM 規范中,真正代表字符串的數據結構是CONSTANT_Utf8_info 結構體,它的結構如下代碼所示:
~~~
CONSTANT_Utf8_info {
u1 tag;
u2 length; //下面就是存儲UTF8 字符串的地方了
u1 bytes[length];
}
大家看圖19中常量池的內容,比如#2=Utf8 TestMain 這行表示:
數組第二個元素的類型是CONSTANT_Utf8_info,字符串為“TestMain”
~~~
**幾個常用的常量池元素類型**
**(1) CONSTANT_Class_info**
這個類型是用于描述類信息的,此處的類信息很簡單,就是類名(也就是代表類名的字符串)
~~~
CONSTANT_Class_info {
u1 tag; //tag 取值為7,代表CONSTANT_Class_info
u2 name_index; //name_index 表示代表自己類名的字符串信息位于于常量池數組中哪一個,也就是索引
}
~~~
name_index 對應的那個常量池元素必須是CONSTANT_Utf8_info,也就是字符串。解析圖19中的例子,咱們再看看:
~~~
#1 = Class #2 // TestMain
#2 = Utf8 TestMain
這說明:
1 常量池第一個元素類型為Class_info,它對應的name_index 取值為2,表示使用第2 個元素
2 常量池第二個元素類型為Utf8 內容為“TestMain”
3 #1 最后的//表示注釋,它把第二行的字符串內容直接搬過來,方便我們查看
~~~
**(2) CONSTANT_NameAndType_Info**
它用來描述方法/成員名以及類型信息的。
有點JNI 基礎的童鞋相信不難明白,在JNI 中,一個類的成員函數或成員變量都可以由這個類名字符串+函數名字符串+參數類型字符串+返回值類型來確定(如果是成員變量,就是類名字符串+變量名字符串+類型字符串)來表達。既然是字符串,那么NameAndType_Info 也就是**存儲了對應字符串在常量池數組中的索引**:
~~~
CONSTANT_NameAndType_info {
u1 tag;
u2 name_index; //方法名或域名對應的字符串索引
u2 descriptor_index; //方法信息(參數+返回值),或者成員變量的信息(類型)對應的字符串索引
}
//還是來看圖19中的例子吧
#13 = Utf8 ()V
#15 = NameAndType #16:#13 // test:()V //合起來就是test.()V 函數名是test,參數和返回值是()V
#16 = Utf8 test
~~~
* * * * *
**注意**,對于構造函數和類初始化函數來說,JVM 要求函數名必須是<init>和<cinit>。當然,這兩個函數是編譯器生成的。
* * * * *
**(3) CONSTANT_MethodrefInfo 三兄弟**
Methodref_Info 還有兩個兄弟,分別是Fieldref_Info,InterfaceMethodref_Info,他們三用于描述方法、成員變量和接口信息。剛才的NameAndType_Info 其實已經描述了方法和成員變量信息的一部分,唯一還缺的就是沒有地方描述它們屬于哪個類。而咱這三兄弟就補全了這些信息。他們三的數據結構如圖20 所示:

:-: 圖20 Methodref,Fieldref 和InterfaceMethoderef 結構
**Field 和Method 描述**
這兩個Info 無非是描述了函數或成員變量的名字,參數,類型等信息。但是真正的方法、成員變量信息還包括比如訪問權限,注解,源代碼位置等。對于方法來說,更重要的還包括其函數功能(即這個函數對應的字節碼)。
在Java VM 中,方法和成員變量的完整描述由如圖21 所示的數據結構來表達的:

:-: 圖21 field_info 和method_info
* access_flags:描述諸如final,static,public 這樣的訪問標志
* name_index:方法或成員變量名在常量池中對應的索引,類型是Utf8_Info
* attribute_info:是域或方法中很重要的信息。我們單獨用一節來介紹它。
**attribute_info 介紹**
attribute_info 結構體很簡單,如下代碼所示:
~~~
attribute_info {//特別注意,這里描述的attribute_info 結構體也是具體屬性數據結構的通用表達
u2 attribute_name_index; //attribute_info 的描述,指向常量池的字符串
u4 attribute_length; //具體的內容由info 數組描述
u1 info[attribute_length];
}
~~~
Java VM 規范中,attribute 類型比較多,我們重點介紹幾個,先來看代表一個函數實際內容的Code 屬性。
**(1) Code 屬性**
代表Code 屬性的數據結構如下圖所示:

:-: 圖22 Code 屬性數據結構
* 前2 個成員變量就不多說了。屬于attribute 的頭6 個字節,分別指向代表屬性名字符串的常量池元素以及后續屬性數據的長度。注意,Code 屬性的attribute_name_index 所指向的那個Utf8 常量池元素對應的字符串內容就是“Code”,大家可參考圖19 的#9。
* max_stack 和max_locals:虛擬機在執行一個函數的時候,會為它建立一個操作數棧。執行過程中的參數啊,一些計算值啊等都會壓入棧中。max_stack 就表示該函數執行時,這個棧的最大深度。這是編譯時就能確定的。max_locals 用于描述這個方法最大的棧數和最大的本地變量個數。本地變量個數包括傳入的參數。
* code_length 和code:這個函數編譯成Java 字節碼后對應的字節碼長度和內容。
* exception_table_length:用來描述該方法對應異常處理的信息。這塊我不打算講了,其實也蠻簡單,就是用start_pc 表示異常處理時候從此方法對應字節碼(由code[]數組表示)哪個地方開始執行。
* Code 屬性本身還能包含一些屬性,這是由attributes_count 和attributes 數組決定的。來看個實際例子吧,如圖7 所示(接著圖19 的例子):

:-: 圖23 main 函數code 屬性解析
圖23中:
* stack=2,locals=2,args_size=1。結合代碼,main 函數確實有一個參數,而且還有一個本地變量。**注意,main 函數是static 的。如果對于類的非static 函數,那么locals 的第0個元素代表this。**
* stack 后面接下來的就是code 數組,也就是這個函數對應的執行代碼。0表示code[]的索引位置。**0:new:代表這個操作是new 操作,此操作對應的字節碼長度為3**,所以下一個操作對應的字節碼從索引3 開始。
* LineNumberTable 也是屬性的一種,用于調試,它將源碼和字節碼匹配了起來。比如**line 6: 0** 這句話代表該函數字節碼0 那一個操作對應代碼的第6 行。
* LocalVariableTable:它也是屬性一種,用于調試,它用于描述函數執行時的變量信息。比如圖23 中的**Start = 0:表示從code[]第0 個字節開始,Length = 21 表示到從start=0 到start+21 個字節**(不包含第21個字節,因為code 數組一共就21 個字節)**這段范圍內,這個變量都有效(也就是這個變量的作用域),Slot=0 表示這個變量在本地變量表中第一個元素,還記得前面提到的locals 嗎?,name 為“args”**,表示這個參數的名字叫args,類型(**由Signature 表示**)就是String 數組了。
* * * * *
另外,Android SDK build Tools 中的dx 工具dump class 文件得到的信息更全,大家可以試試。
使用方法是:dx --dump --debug xxx.class
* * * * *
下圖TestMain.class是通過010Editor工具檢測的結果
[大圖這里](https://box.kancloud.cn/f8460cdd18e474aabb4ca3efc707cac1_605x736.jpg)

:-: 圖24 010Editor檢測TestMain.class結果
[大圖這里 ](https://box.kancloud.cn/6492456d655f453af89b81ed7452c381_1052x897.jpg)
[大圖2這里](https://box.kancloud.cn/e2a3f939eee6ef16bf63ea760dedb426_1052x340.jpg)


:-: 圖25 010Editor檢測TestMain.class的TemplateResult結果
#### **Class文件弊端**
- 內存占用大,不適合移動端。
Class文件包含的東西太多了,一個APP包含幾百個類也不是不可能,且移動端Android手機內存小。
- 堆棧的加棧模式,加載速度慢。
- 文件IO操作多,類查找慢。
### **參考鏈接**:
[談談Java虛擬機——Class文件結構](http://blog.csdn.net/jiangnan_java/article/details/23535247)
[Java虛擬機(四):Class文件結構及字節碼指令](http://blog.csdn.net/Luckydog1991/article/details/51654964)
[Java虛擬機三:Class類文件的結構](http://blog.csdn.net/yulong0809/article/details/77505290)
[《Java虛擬機原理圖解》 1.1、class文件基本組織結構](http://blog.csdn.net/luanlouis/article/details/39892027)
[《Java虛擬機原理圖解》1.3、class文件中的訪問標志、類索引、父類索引、接口索引集合](http://blog.csdn.net/xuchishao/article/details/41211831)
[Java虛擬機工作原理詳解](http://blog.csdn.net/bingduanlbd/article/details/8363734)
- 前言
- Android 熱補丁技術——資源的熱修復
- 插件化系列詳解
- Dex分包——MultiDex
- Google官網——配置方法數超過 64K 的應用
- IMOOC熱修復與插件化筆記
- 第1章 class文件與dex文件解析
- Class文件解析
- dex文件解析
- class與dex對比
- 第2章 虛擬機深入講解
- 第3章 ClassLoader原理講解
- 類的加載過程
- ClassLoade源碼分析
- Android中的動態加載
- 第4章 熱修復簡單講解
- 第5章 熱修復AndFix詳解
- 第6章 熱修復Tinker詳解及兩種方式接入
- 第7章 引入熱修復后代碼及版本管理
- 第8章 插件化原理深入講解
- 第9章 使用Small完成插件化
- 第10章 使用Atlas完成插件化
- 第11章 課程整體總結
- DN學院熱修復插件化筆錄
- 插件化
- 熱修復
- Android APP開發應掌握的底層知識
- 概述
- Binder
- AIDL
- AMS
- Activity的啟動和通信原理
- App啟動流程第2篇
- App內部的頁面跳轉
- Context家族史
- Service
- BroadcastReceiver
- ContentProvider
- PMS及App安裝過程