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