還是熟悉的味道,還是最簡單的代碼。
// Hello.java
public class Hello {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
使用javac編譯 .java 代碼,得到同名的 .class文件,然后使用 java 命令,執行類名就可以運行了。
$ javac Hello.java
$ java Hello
Hello World!
編譯后的class文件,以十六進制格式顯示,長這個樣子:
cafe babe 0000 0034 001d 0a00 0600 0f09
0010 0011 0800 120a 0013 0014 0700 1507
0016 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 046d 6169
6e01 0016 285b 4c6a 6176 612f 6c61 6e67
2f53 7472 696e 673b 2956 0100 0a53 6f75
7263 6546 696c 6501 000a 4865 6c6c 6f2e
6a61 7661 0c00 0700 0807 0017 0c00 1800
1901 000c 4865 6c6c 6f20 576f 726c 6421
0700 1a0c 001b 001c 0100 0548 656c 6c6f
0100 106a 6176 612f 6c61 6e67 2f4f 626a
6563 7401 0010 6a61 7661 2f6c 616e 672f
5379 7374 656d 0100 036f 7574 0100 154c
6a61 7661 2f69 6f2f 5072 696e 7453 7472
6561 6d3b 0100 136a 6176 612f 696f 2f50
7269 6e74 5374 7265 616d 0100 0770 7269
6e74 6c6e 0100 1528 4c6a 6176 612f 6c61
6e67 2f53 7472 696e 673b 2956 0021 0005
0006 0000 0000 0002 0001 0007 0008 0001
0009 0000 001d 0001 0001 0000 0005 2ab7
0001 b100 0000 0100 0a00 0000 0600 0100
0000 0100 0900 0b00 0c00 0100 0900 0000
2500 0200 0100 0000 09b2 0002 1203 b600
04b1 0000 0001 000a 0000 000a 0002 0000
0003 0008 0004 0001 000d 0000 0002 000e
這就是可供Java虛擬機執行的字節碼文件,也是Java之所以能夠實現,一次編譯多次運行的根本原因。
把代碼編譯成一個中間狀態的字節碼,而不是直接運行在操作系統上的機器碼,使得跨系統的工作就完全交給Java虛擬機了。這樣一來,就可以大大減少開發人員的適配工作量,從而提高開發效率,這就是所謂的平臺無關性。
采用Java虛擬機的架構設計,為語言擴展留下了空間,相當于給代碼編譯和操作系統做了一個中間件,只要將代碼編譯成字節碼文件,就都可以運行在Java虛擬機上,這也是為什么會有Scala 、Kotlin、Groovy等多種語言都可以與Java混合編碼的原因,這就是所謂的語言無關性。
既然我們看到了這個字節碼文件,那就得好好解讀一下了。其實只要是代碼編譯的文件,都是有約定的規范格式的,字節碼也是一樣的。
1. class文件結構
類型 | 名稱 | 說明 | 備注或示例對照 |
---|---|---|---|
u4 | magic | 魔數,識別class文件 | cafe babe |
u2 | minor_version | 副版本號 | 0000 |
u2 | major_version | 主版本號 | 0034 |
u2 | constant_pool_count | 常量池計數器 | 001d,十進制值為29 |
cp_info | constant_pool | 常量池 | 常量個數為constant_pool_count-1, 示例即為28個常量 |
u2 | access_flags | 訪問標志 | 0021 |
u2 | this_class | 類索引 | 0005 |
u2 | super_class | 父類索引 | 0006 |
u2 | interfaces_count | 接口計數器 | 0000 |
u2 | interfaces | 接口索引集合 | |
u2 | fields_count | 字段個數 | 0000 |
field_info | fields | 字段集合 | |
u2 | methods_count | 方法計數器 | 0002 |
method_info | methods | 方法集合 | |
u2 | attributes_count | 附加屬性計數器 | 0001 |
attribute_info | attributes | 附加屬性集合 | 000d 0000 0002 000e |
class文件偽結構中只有兩種數據類型:無符號數和表。
無符號數屬于基本的數據類型,以u1、u2、u4和u8來分表代表1個字節、2個字節、4個字節和8個字節的無符號數,無符號數可以用來描述數字、索引引用、數量值或者按照UTF-8編碼構成字符串值。
表是由多個無符號數或者其他表作為數據項構成的復合數據類型,所有表都習慣地以"_info"結尾。表用于描述有層次關系的復合結構的數據,而整個class文件本質上就是一張表。
2. 常量類型
類型 | 標志 | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8編碼的字符串 |
CONSTANT_Integer_info | 3 | 整形字面量 |
CONSTANT_Float_info | 4 | 浮點型字面量 |
CONSTANT_Long_info | 5 | 長整型字面量 |
CONSTANT_Double_info | 6 | 雙精度浮點型字面量 |
CONSTANT_Class_info | 7 | 類或接口的符號引用 |
CONSTANT_String_info | 8 | 字符串類型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符號引用 |
CONSTANT_Methodref_info | 10 | 類中方法的符號引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符號引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的符號引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MothodType_info | 16 | 標識方法類型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一個動態方法調用點 |
3. 常量結構
常量 | 項目 | 類型 | 描述 |
---|---|---|---|
CONSTANT_Utf8_info | tag | u1 | 值為1 |
length | u2 | utf-8編碼的字符串占用的字節數 | |
bytes | u1 | 長度為length的utf-8編碼的字符串 | |
CONSTANT_Integer_info | tag | u1 | 值為3 |
bytes | u4 | 按照高位在前存儲的int值 | |
CONSTANT_Float_info | tag | u1 | 值為4 |
bytes | u4 | 按照高位在前存儲的floatt值 | |
CONSTANT_Long_info | tag | u1 | 值為5 |
bytes | u8 | 按照高位在前存儲的long值 | |
CONSTANT_Double_info | tag | u1 | 值為6 |
bytes | u8 | 按照高位在前存儲double值 | |
CONSTANT_Class_info | tag | u1 | 值為7 |
index | u2 | 指向全限定名常量的索引 | |
CONSTANT_String_info | tag | u1 | 值為8 |
index | u2 | 指向字符串字面量的索引 | |
CONSTANT_Fieldref_info | tag | u1 | 值為9 |
index | u2 | 指向聲明字段的類或者接口描述符CONSTANT_Class_info的索引項 | |
index | u2 | 指向字段描述符CONSTANT_NameAndType_info的索引項 | |
CONSTANT_Methodref_info | tag | u1 | 值為10 |
index | u2 | 指向聲明方法的類描述符CONSTANT_Class_info的索引項 | |
index | u2 | 指向名稱及類型描述符CONSTANT_NameAndType_info的索引項 | |
CONSTANT_InterfaceMethodref_info | tag | u1 | 值為11 |
index | u2 | 指向聲明方法的接口描述符CONSTANT_Class_info的索引項 | |
index | u2 | 指向名稱及類型描述符CONSTANT_NameAndType_info的索引項 | |
CONSTANT_NameAndType_info | tag | u1 | 值為12 |
index | u2 | 指向該字段或方法名稱常量項的索引 | |
index | u2 | 指向該字段或方法描述符常量項的索引 | |
CONSTANT_MethodHandle_info | tag | u1 | 值為15 |
reference_kind | u1 | 值必須是1-9,它決定了方法句柄的類型,方法句柄類型的值表示方法句柄的字節碼行為 | |
reference_index | u2 | 值必須是對常量池的有效索引 | |
CONSTANT_MothodType_info | tag | u1 | 值為16 |
descriptor_index | u2 | 值必須是對常量池的有效索引,常量池在該索引處的項必須是CONSTANT_Utf8_info結構,表示方法的描述符 | |
CONSTANT_InvokeDynamic_info | tag | u1 | 值為18 |
bootstrap_method_attr_index | u2 | 值必須是對當前Class文件中引導方法表示的bootstrap_methods[]數組的有效索引 | |
name_and_type_index | u2 | 值必須是對當前常量池的有效索引,常量池在該索引處的項必須是CONSTANT_NameAndType_info結構,表示方法名和方法描述符 |
人工解讀示例字節碼中的常量池部分
0a 0013 0014 0700 1507
0016 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 046d 6169
6e01 0016 285b 4c6a 6176 612f 6c61 6e67
2f53 7472 696e 673b 2956 0100 0a53 6f75
7263 6546 696c 6501 000a 4865 6c6c 6f2e
6a61 7661 0c00 0700 0807 0017 0c00 1800
1901 000c 4865 6c6c 6f20 576f 726c 6421
0700 1a0c 001b 001c 0100 0548 656c 6c6f
0100 106a 6176 612f 6c61 6e67 2f4f 626a
6563 7401 0010 6a61 7661 2f6c 616e 672f
5379 7374 656d 0100 036f 7574 0100 154c
6a61 7661 2f69 6f2f 5072 696e 7453 7472
6561 6d3b 0100 136a 6176 612f 696f 2f50
7269 6e74 5374 7265 616d 0100 0770 7269
6e74 6c6e 0100 1528 4c6a 6176 612f 6c61
6e67 2f53 7472 696e 673b 2956
index | 十六進制碼 | 長度 | 類型或備注 |
---|---|---|---|
1 | 0a | 1 | CONSTANT_Methodref_info |
0006 | 2 | #6 | |
000f | 2 | #15 | |
2 | 09 | 1 | CONSTANT_Fieldref_info |
0010 | 2 | #10 | |
0011 | 2 | #11 | |
3 | 08 | 1 | CONSTANT_String_info |
0012 | 2 | #18 | |
4 | 0a | 1 | CONSTANT_Methodref_info |
0013 | 2 | #19 | |
0014 | 2 | #20 | |
5 | 07 | 1 | CONSTANT_Class_info |
0015 | 2 | #21 | |
6 | 07 | 1 | CONSTANT_Class_info |
0016 | 2 | #22 | |
7 | 01 | 1 | CONSTANT_Utf8_info |
0006 | 2 | length=6 | |
3c 696e 6974 3e | 6 | <init> | |
8 | 01 | 1 | CONSTANT_Utf8_info |
0003 | 2 | length=3 | |
2829 56 | 3 | ()V | |
9 | 01 | 1 | CONSTANT_Utf8_info |
0004 | 2 | length=4 | |
436f 6465 | 4 | Code | |
10 | 01 | 1 | CONSTANT_Utf8_info |
000f | 2 | length=15 | |
4c 696e 654e 756d 6265 7254 6162 6c65 | 15 | LineNumberTable | |
11 | 01 | 1 | CONSTANT_Utf8_info |
0004 | 2 | ||
6d 6169 6e | 4 | main | |
12 | 01 | 1 | CONSTANT_Utf8_info |
0016 | 2 | ||
285b 4c6a 6176 612f 6c61 6e672f53 7472 696e 673b 2956 | 22 | ([Ljava/lang/String;)V | |
13 | 01 | 1 | CONSTANT_Utf8_info |
000a | 2 | ||
53 6f757263 6546 696c 65 | 10 | SourceFile | |
14 | 01 | 1 | CONSTANT_Utf8_info |
000a | 2 | ||
4865 6c6c 6f2e 6a61 7661 | 10 | Hello.java | |
15 | 0c | 1 | CONSTANT_NameAndType_info |
0007 | 2 | ||
0008 | 2 | ||
16 | 07 | 1 | CONSTANT_Class_info |
0017 | 2 | ||
17 | 0c | 1 | CONSTANT_NameAndType_info |
0018 | 2 | ||
0019 | 2 | ||
18 | 01 | 1 | CONSTANT_Utf8_info |
000c | 2 | ||
4865 6c6c 6f20 576f 726c 6421 | 12 | Hello World! | |
19 | 07 | 1 | CONSTANT_Class_info |
001a | 2 | ||
20 | 0c | 1 | CONSTANT_NameAndType_info |
001b | 2 | ||
001c | 2 | ||
21 | 01 | 1 | CONSTANT_Utf8_info |
0005 | 2 | ||
48 656c 6c6f | 5 | Hello | |
22 | 01 | 1 | CONSTANT_Utf8_info |
0010 | 2 | ||
6a 6176 612f 6c61 6e67 2f4f 626a 6563 74 | 16 | java/lang/Object | |
23 | 01 | 1 | CONSTANT_Utf8_info |
0010 | 2 | ||
6a61 7661 2f6c 616e 672f 5379 7374 656d | 16 | java/lang/System | |
24 | 01 | 1 | CONSTANT_Utf8_info |
0003 | 2 | ||
6f 7574 | 3 | out | |
25 | 01 | 1 | CONSTANT_Utf8_info |
0015 | 2 | ||
4c 6a61 7661 2f69 6f2f 5072 696e 7453 74726561 6d3b | 21 | Ljava/io/PrintStream; | |
26 | 01 | 1 | CONSTANT_Utf8_info |
0013 | 2 | ||
6a 6176 612f 696f 2f50 7269 6e74 5374 7265 616d | 19 | java/io/PrintStream | |
27 | 01 | 1 | CONSTANT_Utf8_info |
0007 | 2 | ||
70 7269 6e74 6c6e | 7 | println | |
28 | 01 | 1 | CONSTANT_Utf8_info |
0015 | 2 | ||
28 4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b 2956 | 21 | (Ljava/lang/String;)V |
4. 類訪問標志
標志名稱 | 標志值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否為public類型 |
ACC_FINAL | 0x0010 | 是否被聲明為final,只有類可以設置 |
ACC_SUPER | 0x0020 | 是否允許使用invokespecial字節碼指令的新語義,JDK1.0.2之后編譯出來的類的這個標志默認為真 |
ACC_INTERFACE | 0x0200 | 標志這是一個接口 |
ACC_ABSTRACT | 0x0400 | 是否為abstract類型,對于接口或者抽象類來說,此標志值為真,其他類型為假 |
ACC_SYNTHETIC | 0x1000 | 標志這個類并非由用戶代碼產生 |
ACC_ANNOTATION | 0x2000 | 標志這是一個注解 |
ACC_ENUM | 0x4000 | 標志這是一個枚舉 |
5. 方法表結構
類型 | 名稱 | 數量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
6. 方法訪問標志
標志名稱 | 標志值 | 含義 |
---|---|---|
ACC_PUBLIC | 0x0001 | 方法是否為public |
ACC_PRIVATE | 0x0002 | 方法是否為private |
ACC_PROTECTED | 0x0004 | 方法是否為protected |
ACC_STATIC | 0x0008 | 方法是否為static |
ACC_FINAL | 0x0010 | 方法是否為final |
ACC_SYHCHRONRIZED | 0x0020 | 方法是否為synchronized |
ACC_BRIDGE | 0x0040 | 方法是否是有編譯器產生的方法 |
ACC_VARARGS | 0x0080 | 方法是否接受參數 |
ACC_NATIVE | 0x0100 | 方法是否為native |
ACC_ABSTRACT | 0x0400 | 方法是否為abstract |
ACC_STRICTFP | 0x0800 | 方法是否為strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否是有編譯器自動產生的 |
7. 通用屬性結構表
類型 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
人工解讀示例字節碼中的方法部分
0002 0001 0007 0008 0001
0009 0000 001d 0001 0001 0000 0005 2ab7
0001 b100 0000 0100 0a00 0000 0600 0100
0000 0100 0900 0b00 0c00 0100 0900 0000
2500 0200 0100 0000 09b2 0002 1203 b600
04b1 0000 0001 000a 0000 000a 0002 0000
0003 0008 0004
十六進制碼 | 類型或備注 | |
---|---|---|
0002 | 方法個數為2 | |
第一個方法 | 0001 | 方法標志為public |
0007 | 方法名索引為7, <init> | |
0008 | 方法描述符索引為8,()V | |
0001 | attribute個數為1 | |
第一個屬性 | 0009 | 屬性索引為9, Code |
0000 001d | 屬性值長度為29 | |
0001 0001 0000 0005 2ab7 0001 b100 0000 0100 0a00 0000 0600 0100 0000 01 | 29個字節 | |
第二個方法 | 00 09 | 方法標志為public,static |
00 0b | 方法名索引為11,main | |
00 0c | 方法描述符索引為12,([Ljava/lang/String;)V | |
00 01 | attribute個數為1 | |
第一個屬性 | 00 09 | 屬性索引為9, Code |
00 0000 25 | 屬性值長度為37 | |
00 0200 0100 0000 09b2 0002 1203 b600 04b1 0000 0001 000a 0000 000a 0002 0000 0003 0008 0004 | 37個字節 |
屬性類型比較多,字段、接口和方法中都存在,但是都可以按照通用結構進行字節拆分。各個屬性有自己的表結構,不做逐一列舉,只列出常用的Code結構做栗子。
8. Code屬性結構表
類型 | 名稱 | 數量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_table_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
逐字節解讀雖然繁瑣,但是能加深對字節碼的結構的理解。不過,在掌握了結構原理之后,使用工具才是最實在的,畢竟人工解讀的效率太低了。
$ javap -verbose Hello.class
Classfile /Users/cage/Study/java_cmd/Hello.class
Last modified 2020-3-15; size 416 bytes
MD5 checksum e2e1c1e330c7df4592041d815080126b
Compiled from "Hello.java"
public class Hello
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // Hello World!
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // Hello
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 Hello.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 Hello World!
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 Hello
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{
public Hello();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 3: 0
line 4: 8
}
SourceFile: "Hello.java"
有興趣的同學,平時可以經常看看字節碼文件及其結構,看都用到了哪些字節碼指令,這樣對理解Java代碼的執行會更有好處。