1. 程式人生 > >Java基礎篇(JVM)——位元組碼詳解

Java基礎篇(JVM)——位元組碼詳解

這是Java基礎篇(JVM)的第一篇文章,本來想先說說Java類載入機制的,後來想想,JVM的作用是載入編譯器編譯好的位元組碼,並解釋成機器碼,那麼首先應該瞭解位元組碼,然後再談載入位元組碼的類載入機制似乎會好些,所以這篇改成詳解位元組碼。

由於Java純面向物件的特性,位元組碼只要能表示一個類的資訊,就可以表示整個Java程式了,JVM只要能載入一個類的資訊,就能載入整個程式了。所以,不管是位元組碼,還是JVM載入機制,關注點都是在類。我關注的點主要在於:

1. 由於位元組碼不是一次性全部載入進入記憶體,那麼JVM是如何知道自己要載入的類資訊在.class檔案的哪個位置的?

2. 位元組碼是如何表示類資訊的?

3. 位元組碼會進行程式的優化嗎?

第一個問題很簡單,因為哪怕一個原始檔有很多個類(只有一個public類),編譯器也會為其中每個類都生成一個.class檔案,JVM載入時按照需要載入的類名稱載入即可。

要解決後面的問題,首先我們來看位元組碼的組成(Mac下用Hex Fiend開啟)。

對這樣一段程式碼:

package com.test.main1;

public class ByteCodeTest {

	int num1 = 1;
	int num2 = 2;

	public int getAdd() {

		return num1 + num2;
	}
}

class Extend extends ByteCodeTest {
	public int getSubstract() {
		return num1 - num2;
	}
}

我們來分析其中的Extend類。

用Hex Fiend開啟編譯後的.class檔案是這樣的(16進位制程式碼):

由於class檔案沒有分隔符,所以每個位置代表什麼、各個部分的長度等格式是嚴格規定死的,見下表:

其中u1、u2、u4、u8代表幾個位元組的無符號數,在反編譯出來的16進位制檔案中,兩個數字代表一個位元組,也就是u1。

從頭到尾一項一項地看:

(1)magic:u4,魔數,代表本檔案是.class檔案。.jpg等也會有這種魔數,正因為魔數存在,即使將*.jpg改成*.123,也能照常開啟。

(2)minor versionmajor version:各u2,版本號,向下相容,即高版本JDK可以使用低版本的.class檔案,反之不行。

(3)constant_pool_count:u2,常量池中常量的數量,0019代表有24個。

(4)接下來就是具體的常量,共constant_pool_count-1個。

常量池通常存兩種型別的資料:

字面量:如字串、final修飾的常量等;

符號引用:如類/介面的全限定名、方法的名稱和描述、欄位的名稱和描述等。

根據反編譯出來的數字,首先查下表得到該常量的型別和長度,接下來的與查得的長度相等的數字則表示該常量具體的值。

如070002,就表示該種類型為CONSTANT_Class_info,它的tag為u1,且接下來u2長度為index指向全限定名常量項的索引。這個索引還要結合javap -verbose開啟的class檔案一起看,這裡清晰地列出了常量池中的內容和順序:

在這裡可以看到0002索引項的常量為:com/test/main1/Extend,是類的全限定名。如果是值是字串,那麼需要根據該值轉換成十進位制並查ASCII碼錶得到具體的字元。接下來的常量都照此分析:

01001563 6F6D2F74 6573742F 6D61696E 312F4578 74656E64:com/test/main1/Extend

070004:com/test/main1/ByteCodeTest

01001B63 6F6D2F74 6573742F 6D61696E 312F4279 7465436F 64655465 7374:com/test/main1/ByteCodeTest

0100063C 696E6974 3E:<init>

01000328 2956:()V

01000443 6F6465:Code

0A000300 09:com/test/main1/ByteCodeTest、"<init>":()V

0C000500 06:<init>、()V

01000F4C 696E654E 756D6265 72546162 6C65:LineNumberTable

0100124C 6F63616C 56617269 61626C65 5461626C 65:LocalVariableTable

01000474 686973:this

0100174C 636F6D2F 74657374 2F6D6169 6E312F45 7874656E 643B:Lcom/test/main1/Extend;

01000C67 65745375 62737472 616374:getSubstract

01000328 2949:()I

09000100 11:com/test/main1/Extend、num1:I

0C001200 13:num1、I

0100046E 756D31:num1

01000149:I

09000100 15:com/test/main1/Extend、num2:I

0C001600 13:num2、I

0100046E 756D32:num2

01000A53 6F757263 6546696C 65:SourceFile

01001142 79746543 6F646554 6573742E 6A617661:ByteCodeTest.java

至此,常量池中的常量全部解析完畢。

(5)再接下來是u2的access_flags:access_flags訪問標誌的主要目的是標記該類是類還是介面,如果是類,訪問許可權是否為public,是否是abstract,是否被標誌為final等,見下表:

Flag_name Value Interpretation
ACC_PUBLIC 0x0001 表示訪問許可權為public,可以從本包外訪問
ACC_FINAL 0x0010 表示由final修飾,不允許有子類
ACC_SUPER 0x0020 較為特殊,表示動態繫結直接父類,見下面的解釋
ACC_INTERFACE 0x0200 表示介面,非類
ACC_ABSTRACT 0x0400 表示抽象類,不能例項化
ACC_SYNTHETIC 0x1000 表示由synthetic修飾,不在原始碼中出現,見附錄[2]
ACC_ANNOTATION 0x2000 表示是annotation型別
ACC_ENUM 0x4000 表示是列舉型別

 所以,本類中的access_flags是0020,表示這個Extend類呼叫父類的方法時,並非是編譯時繫結,而是在執行時搜尋類層次,找到最近的父類進行呼叫。這樣可以保證呼叫的結果是一定是呼叫最近的父類,而不是編譯時繫結的父類,保證結果的正確性。這個可以參見文章[1]

(6)this_class:u2的類索引,用於確定類的全限定名。本類的this_class是0001,表示在常量池中#1索引,是com/test/main1/Extend

(7)super_class:u2的父類索引,用於確定直接父類的全限定名。本類是0003,#3是com/test/main1/ByteCodeTest

(8)interfaces_count:u2,表示當前類實現的介面數量,注意是直接實現的介面數量。本類中是0000,表示沒有實現介面。

(9)Interfaces:表示介面的全限定名索引。每個介面u2,共interfaces_count個。本類為空。

(10)fields_count:u2,表示類變數和例項變數總的個數。本類中是0000,無。

(11)fields:fileds的長度為filed_info,filed_info是一個複合結構,組成如下:

filed_info: {

  u2        access_flags;   

  u2        name_index;

  u2        descriptor_index;      

  u2        attributes_count;

  attribute_info   attributes[attributes_count];

}

由於本類無類變數和例項變數,故本欄位為空。

(12)methods_count:u2,表示方法個數。本類中是0002,表示有2個。

(13)methods:methods的長度為一個method_info結構:

method_info {

  u2        access_flags;          0000    ?

  u2        name_index;           0005   <init>

  u2        descriptor_index;         0006    ()V

  u2        attributes_count;         0001    1個

  attribute_info   attributes[attributes_count];   0007    Code

}

其中attribute_info結構如下:

attribute_info {

  u2  attribute_name_index;  0007  Code

  u1  attribute_length;

  u1  info[attribute_length];

}

上面?是通用的attribute_info的定義,另外,JVM裡預定義了幾種attribute,Code即是其中一種(注意,如果使用的是JVM預定義的attribute,則attribute_info的結構就按照預定義的來),其結構如下:

Code_attribute {            //Code_attribute包含某個方法、例項初始化方法、類或介面初始化方法的Java虛擬機器指令及相關輔助資訊

  u2  attribute_name_index;  0007         Code

  u4  attribute_length;    0000002F          47

  u2  max_stack;       0001          1      //用來給出當前方法的運算元棧在方法執行的任何時間點的最大深度

  u2  max_locals;      0001           1      //用來給出分配在當前方法引用的區域性變量表中的區域性變數個數

  u4  code_length;      00000005        5      //給出當前方法code[]陣列的位元組數

  u1  code[code_length];    2AB70008 B1       42、183、0、8、177 //給出了實現當前方法的Java虛擬機器程式碼的實際位元組內容 

                                       (這些數字程式碼實際對應一些Java虛擬機器的指令)

  u2  exception_table_lentgh;   0000         0          //異常的資訊

  {

    u2  start_pc;   //這兩項的值表明了異常處理器在code[]中的有效範圍,即異常處理器x應滿足:start_pc≤x≤end_pc

    u2  end_pc;   //start_pc必須在code[]中取值,end_pc要麼在code[]中取值,要麼等於code_length的值

    u2  handler_pc; //表示一個異常處理器的起點

    u2  catch_type; //用以表示當前異常處理器需要捕捉的異常型別。如果為0,則都會呼叫這個異常處理器,可以用來實現finally。

  } exception_table[exception_table_lentgh];      在本類中大括號裡的結構為空

  u2  attribute_count;    0002          2    表示該方法的其它附加屬性,本類有1個

  attribute_info  attributes[attributes_count];       000A、000B     LineNumberTable、LocalVariableTable

}

LineNumberTable和LocalVariableTable又是兩個預定義的attribute,其結構如下:

LineNumberTable_attribute {   //被偵錯程式用來確定原始檔中由給定的行號所表示的內容,對應於Java虛擬機器code[]陣列的哪部分

  u2  attribute_name_index;      000A

  u4  attribute_length;          00000006

  u2  line_number_table_length;     0001

  {  u2  start_pc;          0000    

     u2  line_number;         000E     //該值必須與原始檔中對應的行號相匹配

  } line_number_table[line_number_table_length];

}

以及:

LocalVariableTable_attribute {

  u2  attribute_name_index;    000B

  u4  attribute_length;        0000000C

  u2  local_variable_table_length;   0001

  {  u2  start_pc;          0000

     u2  length;           0005

     u2  name_index;       000C

     u2  descriptor_index;    000D    //用來表示源程式中區域性變數型別的欄位描述符

    u2   index;          0000

  } local_variable_table[local_variable_table_length];

然後就是第二個方法,具體略過。

(14)attributes_count:u2,這裡的attribute表示整個class檔案的附加屬性,和前面方法的attribute結構相同。本類中為0001。

(15)attributes:class檔案附加屬性,本類中為0017,指向常量池#17,為SourceFile,SourceFile的結構如下:

SourceFile_attribute {

  u2  attribute_name_index;  0017      SourceFile

  u4  attribute_length;      00000002    2

  u2  sourcefile_index;        0018      ByteCodeTest.java //表示本class檔案是由ByteCodeTest.java編譯來的

}

嗯,位元組碼的內容大概就寫這麼多。可以看到通篇文章基本都是在分析位元組碼檔案的16進位制程式碼,所以可以這麼說,位元組碼的核心在於其16進位制程式碼,利用規範中的規則去解析這些程式碼,可以得出關於這個類的全部資訊,包括:

1. 這個類的版本號;

2. 這個類的常量池大小,以及常量池中的常量;

3. 這個類的訪問許可權;

4. 這個類的全限定名、直接父類全限定名、類的直接實現的介面資訊;

5. 這個類的類變數和例項變數的資訊;

6. 這個類的方法資訊;

7. 其它的這個類的附加資訊,如來自哪個原始檔等。

解析完位元組碼,回頭再來看開始提出的問題,也就迎刃而解了。由於位元組碼檔案格式嚴格按照規定,可以用來表示類的全部資訊;位元組碼只是用來表示類資訊的,不會進行程式的優化。

那麼在編譯期間,編譯器會對程式進行優化嗎?執行期間JVM會嗎?什麼時候進行的,按照什麼原則呢?這個留作以後再表。

最後,值得注意的是,位元組碼不僅是平臺無關的(任何平臺生成的位元組碼都可以在任何的JRE環境執行),還是語言無關的,不僅Java可以生成位元組碼,其它語言如Groovy、Jython、Scala等也能生成位元組碼,執行在JRE環境中。

參考文章

[1] https://blog.csdn.net/xinaij/article/details/38872851

[2] synthetic關鍵字不是人為新增的,而是編譯器基於程式邏輯自動新增的,可以修飾方法,也可以修飾類。通常出現在有內部類,且內部類訪問許可權為private的時候。

我們可以在外部類中呼叫內部類的private方法,訪問private屬性。但其實編譯器對所有的類包括內部類,都是當做頂級類來編譯的,這就是說一個頂級類可以訪問另一個頂級類的私有方法,顯然有問題。為了不出錯,編譯器對內部類的私有屬性都加上了synthetic修飾的access方法,類似於setter/getter方法,使得外部類可以訪問內部類的私有屬性。私有方法也一樣,加了一個具有包訪問許可權的方法,呼叫私有方法,使得外部類可以呼叫私有方法。

當內部類的訪問許可權為private的話,照理來說只能本類訪問,你是不可能在程式其它地方通過OuterClass.InnerClass來new一個內部類物件的,但是我們經常這麼做,而且還沒出錯,原因就是編譯器幫我們合成了一個具有包訪問許可權的合成類(也就是具有包訪問許可權的構造器)。這個還不是很清楚,但是大體的思路應該與私有屬性和方法類似。

https://blog.csdn.net/zhang_yanye/article/details/50301511

https://www.cnblogs.com/bethunebtj/p/7761596.html

[3] 之前看別人的文章我一直有個疑問,他們這些知識是哪裡來的?現在慢慢搞明白了,很多都是規範上擷取的。比如這篇,我就參考了很多《Java虛擬機器規範》中的內容。授人以魚不如授人以漁,感興趣可以翻翻這篇。