还是熟悉的味道,还是最简单的代码。
// 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代码的执行会更有好处。
网友评论