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虛擬機規范》中的內容。授人以魚不如授人以漁,感興趣可以翻翻這篇。


免責聲明!

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



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