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