上文介绍了字节码的结构,本文主要通过一个简单的例子说明class字节码的每一个字段。
package com.zcm.test;
import java.io.Serializable;
public class SourceTest implements Serializable{
public int a=3;
static Integer b=6;
String s="Hello World!";
public static void main(String[] args){
SourceTest test=new SourceTest();
test.a=9;
b=8;
}
private String test(){
return s;
}
}
使用javac -d SourceTest.java 编译该文件,生成SourceTest.class。
使用javap -verbose SourceTest 就会分析出该class的字节码内容
Classfile /Users/zcm/Downloads/com/zcm/test/SourceTest.class
Last modified 2018-4-2; size 694 bytes
MD5 checksum 5874dd9682f495b853d6ccb7b8af652b
Compiled from "SourceTest.java"
public class com.zcm.test.SourceTest implements java.io.Serializable
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#28 // java/lang/Object."":()V
#2 = Fieldref #5.#29 // com/zcm/test/SourceTest.a:I
#3 = String #30 // Hello World!
#4 = Fieldref #5.#31 // com/zcm/test/SourceTest.s:Ljava/lang/String;
#5 = Class #32 // com/zcm/test/SourceTest
#6 = Methodref #5.#28 // com/zcm/test/SourceTest."":()V
#7 = Methodref #33.#34 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#8 = Fieldref #5.#35 // com/zcm/test/SourceTest.b:Ljava/lang/Integer;
#9 = Class #36 // java/lang/Object
#10 = Class #37 // java/io/Serializable
#11 = Utf8 a
#12 = Utf8 I
#13 = Utf8 b
#14 = Utf8 Ljava/lang/Integer;
#15 = Utf8 s
#16 = Utf8 Ljava/lang/String;
#17 = Utf8
#18 = Utf8 ()V
#19 = Utf8 Code
#20 = Utf8 LineNumberTable
#21 = Utf8 main
#22 = Utf8 ([Ljava/lang/String;)V
#23 = Utf8 test
#24 = Utf8 ()Ljava/lang/String;
#25 = Utf8
#26 = Utf8 SourceFile
#27 = Utf8 SourceTest.java
#28 = NameAndType #17:#18 // "":()V
#29 = NameAndType #11:#12 // a:I
#30 = Utf8 Hello World!
#31 = NameAndType #15:#16 // s:Ljava/lang/String;
#32 = Utf8 com/zcm/test/SourceTest
#33 = Class #38 // java/lang/Integer
#34 = NameAndType #39:#40 // valueOf:(I)Ljava/lang/Integer;
#35 = NameAndType #13:#14 // b:Ljava/lang/Integer;
#36 = Utf8 java/lang/Object
#37 = Utf8 java/io/Serializable
#38 = Utf8 java/lang/Integer
#39 = Utf8 valueOf
#40 = Utf8 (I)Ljava/lang/Integer;
{
public int a;
descriptor: I
flags: ACC_PUBLIC
static java.lang.Integer b;
descriptor: Ljava/lang/Integer;
flags: ACC_STATIC
java.lang.String s;
descriptor: Ljava/lang/String;
flags:
public com.zcm.test.SourceTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: iconst_3
6: putfield #2 // Field a:I
9: aload_0
10: ldc #3 // String Hello World!
12: putfield #4 // Field s:Ljava/lang/String;
15: return
LineNumberTable:
line 5: 0
line 6: 4
line 8: 9
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #5 // class com/zcm/test/SourceTest
3: dup
4: invokespecial #6 // Method "":()V
7: astore_1
8: aload_1
9: bipush 9
11: putfield #2 // Field a:I
14: bipush 8
16: invokestatic #7 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
19: putstatic #8 // Field b:Ljava/lang/Integer;
22: return
LineNumberTable:
line 11: 0
line 12: 8
line 13: 14
line 14: 22
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 6
2: invokestatic #7 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: putstatic #8 // Field b:Ljava/lang/Integer;
8: return
LineNumberTable:
line 7: 0
}
SourceFile: "SourceTest.java"
通过javap -verbose命令可以看到字节码所包含的信息。
在Mac系统下可以通过vim查看class文件,键入:%!xxd使用十六进制查看
所有的.class字节码文件的开始4个字节都是魔数,并且其值一定是0xCAFEBABE。如果该值不是0xCAFEBABE,则JVM拒绝加载该文件。
魔数之后的4个字节为版本信息,前2个字节表示major version,即主版本号;后2个字节表示minor version,即次版本号。
这里的版本号值为0x00000034对应的十进制数是52,这里我用Java1.8编译出来的,其值是52。如果class文件的版本号超出了虚拟机支持的最高版本号,JVM将抛出java.lang.UnsupportedClassVersionError异常。
Java类中的绝大多数信息都由常量池描述,常量池主要由常量池数量和常量池数组俩部分组成。常量池数量紧跟在次版本号的后面,占2个字节,class字节码的排列非常紧凑。而常量池数组紧跟在常量池数量之后。
每一个常量池元素都由两部分组成:tag和数组内容。使用结构化的方式来描述常量池数组的编排,描述如下:
tag1元素内容tag2元素内容tag3元素内容tag4元素内容....
常量池一共定义了11中常量,如下表:
编号 | 常量池元素名称 | tag位标识 | 含义 |
1 | CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 |
2 | CONSTANT_Integer_info | 3 | 整型字面量 |
3 | CONSTANT_Float_info | 4 | 浮点型字面量 |
4 | CONSTANT_Long_info | 5 | 长整型字面量 |
5 | CONSTANT_Double_info | 6 | 双精度字面量 |
6 | CONSTANT_Class_info | 7 | 类或接口的符号引用 |
7 | CONSTANT_String_info | 8 | 字符串类型的字面量 |
8 | CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
9 | CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
10 | CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
11 | CONSTANT_NameAndType_info | 12 | 字段和方法的名称以及类型的符号引用 |
根据前面介绍的字节码格式,紧接着的第9,10字节是常量池的数量,后面是常量池数组内容。
下面给出JVM所定义的常量池中每一种元素的具体结构。
CONSTANT_Utf8_info |
||
类型 |
结构 |
说明 |
u1 |
tag |
CONSTANT_Utf8 (1) |
u2 |
length |
bytes所代表的字符串的长度 |
u1 |
bytes[length] |
字符串的byte数据 |
CONSTANT_Integer_info |
||
类型 |
结构 |
说明 |
u1 |
tag |
CONSTANT_Integer (3) |
u4 |
bytes |
整型常量值 |
CONSTANT_Float_info |
||
类型 |
结构 |
说明 |
u1 |
tag |
CONSTANT_Float(4) |
u4 |
bytes |
单精度浮点型常量值 |
CONSTANT_Long_info |
||
类型 |
结构 |
说明 |
u1 |
tag |
CONSTANT_Long (5) |
u4 |
high_bytes |
长整型的高四位值 |
u4 |
low_bytes |
长整型的低四位值 |
CONSTANT_Double_info |
||
类型 |
结构 |
说明 |
u1 |
tag |
CONSTANT_Double(6) |
u4 |
high_bytes |
双精度浮点的高四位值 |
u4 |
low_bytes |
双精度浮点的低四位值 |
CONSTANT_Class_info format |
||
类型 |
结构 |
说明 |
u1 |
tag |
CONSTANT_Class (7) |
u2 |
name_index |
constant_pool中的索引,CONSTANT_Utf8_info类型。表示类或接口名。 |
CONSTANT_String_info |
||
类型 |
结构 |
说明 |
u1 |
tag |
CONSTANT_String(8) |
u2 |
string_index |
constant_pool中的索引,CONSTANT_Utf8_info类型。表示String类型值。 |
CONSTANT_Fieldref_info |
||
类型 |
结构 |
说明 |
u1 |
tag |
CONSTANT_Fieldref(9) |
u2 |
class_index |
constant_pool中的索引,CONSTANT_Class_info类型。记录定义该字段的类或接口。 |
u2 |
name_and_type_index |
constant_pool中的索引,CONSTANT_NameAndType_info类型。指定类或接口中的字段名(name)和字段描述符(descriptor)。 |
CONSTANT_Methodref_info |
||
类型 |
结构 |
说明 |
u1 |
tag |
CONSTANT_Methodref(10) |
u2 |
class_index |
constant_pool中的索引,CONSTANT_Class_info类型。记录定义该方法的类。 |
u2 |
name_and_type_index |
constant_pool中的索引,CONSTANT_NameAndType_info类型。指定类中扽方法名(name)和方法描述符(descriptor)。 |
CONSTANT_InterfaceMethodref_info |
||
类型 |
结构 |
说明 |
u1 |
tag |
CONSTANT_InterfaceMethodref(11) |
u2 |
class_index |
constant_pool中的索引,CONSTANT_Class_info类型。记录定义该方法的接口。 |
u2 |
name_and_type_index |
constant_pool中的索引,CONSTANT_NameAndType_info类型。指定接口中的方法名(name)和方法描述符(descriptor)。 |
CONSTANT_NameAndType_info |
||
类型 |
结构 |
说明 |
u1 |
tag |
CONSTANT_NameAndType (12) |
u2 |
name_index |
constant_pool中的索引,CONSTANT_Utf8_info类型。指定字段或方法的名称。 |
u2 |
descriptor_index |
constant_pool中的索引,CONSTANT_Utf8_info类型。指定字段或方法的描述符 |
接着上面的分析,后面跟在常量池数量后面的就是常量池数组,按照上面JVM常量表的顺序排列。这里就不一一描述了。
常量池后面紧跟的就是access_flags结构,该结构占2个字节,类型是u2。代表访问标志位,用于标注类或接口的访问信息。access_flags的可选值如下:
标志名称 | 值 | 含义 |
ACC_PUBLIC | 0×0001 | 是否为pubilc类型 |
ACC_FINAL | 0×0010 | 是否为final类型,只有类可设置 |
ACC_SUPER | 0×0020 | 用于兼容早期编译器,新编译器都设置该标记,以在使用invokespecial指令时对子类方法做特定处理。 |
ACC_INTERFACE | 0×0200 | 标识为接口。不可同时设置:ACC_FINAL、ACC_SUPER、ACC_ENUM |
ACC_ABSTRACT | 0×0400 | 抽象类,无法实例化。不可与ACC_FINAL同时设置。 |
ACC_SYNTHETIC | 0×1000 | synthetic,由编译器产生,不存在于源代码中。 |
ACC_ANNOTATION | 0×2000 | 注解类型(annotation),需同时设置:ACC_INTERFACE、ACC_ABSTRACT |
ACC_ENUM | 0×4000 | 枚举类型 |
该结构类型是u2,占2个字节。其值指向常量池对应的索引值。值是CONSTANT_Class_info类型,记录当前类的全限定名。
该结构类型是u2,占2个字节。其值指向常量池对应的索引值。值是CONSTANT_Class_info类型,记录父类的全限定名。
interfaces包含interfaces_count和interfaces[interfaces_count]。interfaces_count类型是u2,占2个字节。记录当前类所实现的接口数量。
interfaces[interfaces_count]是一组u2类型数据的集合。记录了当前类实现了哪些接口。如果没有实现过接口,就没有interface这一项。
fields_count结构同上,记录了当前类所定义的变量的总数量。包括类成员变量和类变量(静态变量)。
field_info |
||
类型 |
名称 |
说明 |
u2 |
access_flags |
记录字段的访问权限。 |
u2 |
name_index |
constant_pool中的索引,CONSTANT_Utf8_info类型。表示变量的名称引用。 |
u2 |
descriptor_index |
constant_pool中的索引,CONSTANT_Utf8_info类型,表示变量的类型信息引用。 |
u2 |
attributes_count |
attributes包含的项目数。 |
attribute_info |
attributes[attributes_count] |
字段中包含的Attribute集合。 |
其中access_flags有如下的可选项。
字段的访问权限 |
||
标志名称 |
值 |
含义 |
ACC_PUBLIC |
0x0001 |
pubilc,包外可访问。 |
ACC_PRIVATE |
0x0002 |
private,只可在类内访问。 |
ACC_PROTECTED |
0x0004 |
protected,类内和子类中可访问。 |
ACC_STATIC |
0x0008 |
static,静态。 |
ACC_FINAL |
0x0010 |
final,常量。 |
ACC_VOILATIE |
0x0040 |
volatile,直接读写内存,不可被缓存。不可和ACC_FINAL一起使用。 |
ACC_TRANSIENT |
0x0080 |
transient,在序列化中被忽略的字段。 |
ACC_SYNTHETIC |
0x1000 |
synthetic,由编译器产生,不存在于源代码中。 |
ACC_ENUM |
0x4000 |
enum,枚举类型字段 |
method_info包含methods_count和methods[methods_count];methods_count类型是u2,占2个字节。描述类中共包含多少个方法。methods_count后面是methods数组。该数组格式如下表:
methods结构 |
||
类型 |
名称 |
说明 |
u2 |
access_flags |
记录方法的访问权限。 |
u2 |
name_index |
constant_pool中的索引,CONSTANT_Utf8_info类型。指定方法名称。 |
u2 |
descriptor_index |
constant_pool中的索引,CONSTANT_Utf8_info类型,指定方法的描述符。 |
u2 |
attributes_count |
attributes包含的项目数。 |
attribute_info |
attributes[attributes_count] |
字段中包含的Attribute集合。 |
methods数组各项结构与fields类似,这里不再赘述。
其中,access_flags的可选值如下:
方法的访问权限 |
||
标志名称 |
标志值 |
说明 |
ACC_PUBLIC |
0x0001 |
pubilc,包外可访问。 |
ACC_PRIVATE |
0x0002 |
private,只可在类内访问。 |
ACC_PROTECTED |
0x0004 |
protected,类内和子类中可访问。 |
ACC_STATIC |
0x0008 |
static,静态。 |
ACC_FINAL |
0x0010 |
final,不可被重写。 |
ACC_SYNCHRONIZED |
0x0020 |
synchronized,同步方法。 |
ACC_BRIDGE |
0x0040 |
bridge方法,由编译器生成。 |
ACC_VARARGS |
0x0080 |
包含不定参数个数的方法。 |
ACC_NATIVE |
0x0100 |
native,非Java语言实现的方法。 |
ACC_ABSTRACT |
0x0400 |
abstract,抽象方法。 |
ACC_STRICT |
0x0800 |
strictfp,设置floating-point模式为FP-strict。 |
ACC_SYNTHETIC |
0x1000 |
synthetic,由编译器产生,不存在于源代码中。 |
属性表集合
在class文件中,属性表,方法表中都可以包含自己的属性表,用于描述某些场景的专有信息。为了能正确解析class文件,虚拟机规范中预定义了虚拟机实现必须能够识别的几种属性,除此以外,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。
属性名称 | 使用位置 | 含义 |
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类文件,字段表,方法表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
SourceFile | 类文件 | 源文件名称 |
Synthetic | 类文件,字段表,方法表 | 标识方法或字段是由编译器自动生成的 |
到此,我们分析了一个具体的Java程序生成的字节码文件的格式和内容。了解class文件结构可以帮助我们更深入的理解JVM的运行原理。本文限于篇幅,很多细节的地方没有分析到位,但是JVM定义的内容大致都有介绍,详细的信息大家可以参考JVM官方文档。