java代码是通过java编译器编译成class文件,然后由jvm加载执行的,jvm屏蔽了底层平台系统执行细节,所以可以做到Compile Once,Run Anywhere。
编译后的class文件,是一个二进制流文件,例如下面的类:
public class ServiceResult<T> { private static final int SUCCESS_CODE = 200; private static final String SUCCESS_MESSAGE = "Success"; private int code; private String message; private T data; public ServiceResult(T data) { this.code = SUCCESS_CODE; this.message = SUCCESS_MESSAGE; this.data = data; } public ServiceResult(int code, String message, T data) { this.code = code; this.message = message; this.data = data; } public boolean isSuccess() { return code == SUCCESS_CODE; } public int getCode() { return code; } public String getMessage() { return message; } public T getData() { return data; } @Override public String toString() { final StringBuilder sb = new StringBuilder("ServiceResult{"); sb.append("code=").append(code); sb.append(", message='").append(message).append('/''); sb.append(", data=").append(data); sb.append('}'); return sb.toString(); } } 复制代码
编译后得到的class,以16进制格式打开如下:
注:class文件以字节(8比特)为单位,用u1,u2,u4,u8分别表示1个字节,2个字节,4个字节,8个字节的无符号数,采用Big-edian形式,即高位字节在前。
二进制class文件如果用类c语言结构体的形式来描述其逻辑结构,则如下图所示:
从图中可知,class文件主要包含magic,minor version,major version,constant pool,access flags,this_class,super class,interfaces,fields,methods,attributes 11个部分,每个部分之间紧凑的拼接在一起,没有分界符分割,下面分别介绍每个结构。
在开始介绍各个结构之前,需要说明本文以jvm1.8为准
本文有些长,这里排版看起来更舒服些
魔数:占4个字节的无符号数,固定为0xCAFEBABE,用来标识改文件是一个class文件
次版本号:占两个字节的无符号数,范围0~65535,与major version一起表示当前class文件的版本,jvm可以向前兼容之前的版本,但不能向后兼容,即jdk7的虚拟机不能运行jdk8编译的class
主版本号:占两个字节的无符号数,jdk1.1使用的主版本号是45,以后每个大版本加1,如jdk1.8为52
常量池:常量池是class中十分重要的一部分,它可不是只保存着类中定义的常量而已,还保存着class文件中的各种元数据,包括一些字符串,类名,接口名,字段名,方法名等等……,它的作用就是被引用,常量池部分首先有两个字节u2记录它包含的常量个数。
PS1:常量池就是一系列常量的数组,它的下标是从1开始的,即有效大小是constant_pool_count-1,第0项是无效的,有些结构可以用索引0来表示没有对常量的引用
PS2:常量池的设计有效的减小的class文件的大小,想想那些重复使用的类名称,字符串现在只需保留一份,并且引用的地方只需要用u2保存它在常量池中的索引就可以了
因为每个常量都有一种具体的类型来代表不同的含义,光知道常量的个数还没办法解析出具体的常量项来,所以定义每个常量的第一个字节u1表示该常量的类型tag,然后就可以根据该类型常量的存储结构来解析了。
常量的tag有CONSTANT_Utf8,CONSTANT_Integer,CONSTANT_Float,CONSTANT_Long,CONSTANT_Double,CONSTANT_Class,CONSTANT_String,CONSTANT_Fieldref,CONSTANT_Methodref,CONSTANT_InterfaceMethodref,CONSTANT_NameAndType,CONSTANT_MethodHandle,CONSTANT_MethodType,CONSTANT_InvokeDynamic等14种,下面对每种类型结构(类型+“_info”)作下介绍:
常量池中最基本的常量,用来保存一个utf8编码字符串,如常量字符串,类名,字段名,方法名等的值都是一个对它的引用(索引)
CONSTANT_Utf8_info { u1 tag; u2 length; u1 bytes[length]; } 复制代码
tag=1,length表示字符串字节长度,如length=20,则表示接下来20个bytes是一个utf8编码的字符串。
这里补充两点:
java使用的是可变utf8编码:ASCII 字符(' /u0001
' ~ ' /u007F
',即1~127)用1个字节表示,null(' /u0000
')和 ' /u0080
' 到 ' /u07FF
'之间的字符用2个字节表示, ' /u0800
' 到 ' /uFFFF
'之间的字符用3个字节表示。
逆向来看就是如果读到一个字节最高位是0,则是一个单字节字符。
读到一个字节最高3位是 110
则是一个双字节字符,紧接着还要再读1个字节。
读到一个字节最高4位是 1110
,则是一个三字节字符,紧接着还要再读2个字节。
关于如何解码可以查看官方文档,在java中,我们只需要使用 new String(bytes, StandardCharset.UTF8)
即可得到解码字符串
length使用了u2(0-65535)来表示,则其表示的字符串最大长度为65535
CONSTANT_Integer_info { u1 tag; u4 bytes; } 复制代码
int,tag=3,接下来4个字节表示该int的值。关于CONSTANT_Integer补充以下几点:
big-endian,字节高位在前,下文同理
如果自己解析则要像下面这样:
int value = 0; byte[] data = new byte[4]; is.read(data); value = (value | (((int) data[0]) & 0xff)) << Byte.SIZE * 3; value = (value | (((int) data[1]) & 0xff)) << Byte.SIZE * 2; value = (value | (((int) data[2]) & 0xff)) << Byte.SIZE; value = (value | (((int) data[3]) & 0xff)); 复制代码
我们可以使用DataInputStream的readInt()方法读取一个int值。
java中 short
, char
, byte
, boolean
使用int来表示,boolean数组则用byte数组来表示(1个byte表示1个boolean元素)
CONSTANT_Float_info { u1 tag; u4 bytes; } 复制代码
float浮点数,tag=4,接下来4个字节表示它的值,采用 IEEE 754标准定义。可以使用DataInputStream的readFloat()方法读取一个float值。
CONSTANT_Long_info { u1 tag; u4 high_bytes; u4 low_bytes; } 复制代码
tag=5,长整数,long和double在class中用两个部分(高位4字节,地位4字节)保存。可以使用DataInputStream的readLong()方法读取一个float值。
CONSTANT_Double_info { u1 tag; u4 high_bytes; u4 low_bytes; } 复制代码
tag=6,双精度浮点数,采用 IEEE 754标准定义。存储同CONSTANT_Long一样。
CONSTANT_Class_info { u1 tag; u2 name_index; } 复制代码
tag=7,表示一个类或接口,注意不是field的类型或method的参数类型、返回值类型。name_index是常量池索引,该索引处常量肯定是一个 CONSTANT_Utf8_info
CONSTANT_String_info { u1 tag; u2 string_index; } 复制代码
tag=8,表示一个常量字符串,string_index是常量池索引,该索引处常量肯定是一个 CONSTANT_Utf8_info
,存储着该字符串的值
CONSTANT_Fieldref_info { u1 tag; u2 class_index; u2 name_and_type_index; } 复制代码
tag=9,表示一个引用field信息,包括静态field和实例field。
class_index是常量池中一个CONSTANT_Class_info类型常量(类/接口)索引,表示field所属类。name_and_type_index是常量池中一个CONSTANT_NameAndType_info(见下文)类型常量索引,表示field的名称和类型。
关于field引用解释一下,包括下面的method,接口method引用同理:
code field_info
CONSTANT_Methodref_info { u1 tag; u2 class_index; u2 name_and_type_index; } 复制代码
tag=10,表示一个引用method信息,包括静态method和实例method。
class_index是常量池中一个CONSTANT_Class_info类型常量(这里只能是类)索引,表示method所属类。name_and_type_index是常量池中一个CONSTANT_NameAndType_info类型常量索引,表示method的名称和参数,返回值信息。
CONSTANT_InterfaceMethodref_info { u1 tag; u2 class_index; u2 name_and_type_index; } 复制代码
tag=11,表示一个接口method信息。
class_index是常量池中一个CONSTANT_Class_info类型常量(这里只能是接口)索引,表示method所属接口。name_and_type_index同CONSTANT_Methodref_info。
CONSTANT_NameAndType_info { u1 tag; u2 name_index; u2 descriptor_index; } 复制代码
tag=12,存储field或method的名称,类型等信息,可以看出它又是两个引用。name_index指向一个CONSTANT_Utf8_info,表示字段或方法的 非全限定名称 。descriptor_index也指向一个CONSTANT_Utf8_info,表示该字段/方法的描述信息。
descriptor用一个字符串CONSTANT_Utf8_info保存。
字段描述符( FieldType ),FieldType可以是基本类型: B(byte)
C(char)
D(double)
F(float)
I(int)
J(long)
S(short)
Z(boolean)
,对象类型:L+全限定类名,数组类型:[+元素类型
int a; // I Integer b; //Ljava/lang/Integer double[] c; //[D double[][] d; //[[D Object[] e; //[Ljava/lang/Object Object[][][] f; //[[[Ljava/lang/Object 复制代码
方法描述符( MethodDescriptor ),MethodDescriptor格式为 (参数类型)返回类型
/** * 描述符:(IDLjava/lang/Thread;)Ljava/lang/Object; */ Object m(int i, double d, Thread t) {...} 复制代码
CONSTANT_MethodHandle_info { u1 tag; u1 reference_kind; u2 reference_index; } 复制代码
tag=15,方法句柄,比如获取一个类静态字段,实例字段,调用一个方法,构造器等都会转化成一个句柄引用。
reference_kind
Kind | Description | Interpretation |
---|---|---|
1 | REF_getField |
getfield C.f:T |
2 | REF_getStatic |
getstatic C.f:T |
3 | REF_putField |
putfield C.f:T |
4 | REF_putStatic |
putstatic C.f:T |
5 | REF_invokeVirtual |
invokevirtual C.m:(A*)T |
6 | REF_invokeStatic |
invokestatic C.m:(A*)T |
7 | REF_invokeSpecial |
invokespecial C.m:(A*)T |
8 | REF_newInvokeSpecial |
new C; dup; invokespecial C.<init>:(A*)V |
9 | REF_invokeInterface |
invokeinterface C.m:(A*)T |
f: field,m: method,:实例构造器
reference_index
CONSTANT_MethodType_info { u1 tag; u2 descriptor_index; } 复制代码
tag=16,描述一个方法类型。descriptor_index引用一个CONSTANT_Utf8_info,表示方法的描述符
CONSTANT_InvokeDynamic_info { u1 tag; u2 bootstrap_method_attr_index; u2 name_and_type_index; } 复制代码
tag=18,invokedynamic动态调用指令引用信息。
access flags表示类,接口,字段,方法的访问控制和修饰信息。
Access Flag(u2) | Value | 作用对象 |
---|---|---|
ACC_PUBLIC | 0x0001 | class, inner, field, method |
ACC_PRIVATE | 0x0002 | inner, field, method |
ACC_PROTECTED | 0x0004 | inner, field, method |
ACC_STATIC | 0x0008 | inner, field, method |
ACC_FINAL | 0x0010 | class, inner, field, method |
ACC_SUPER | 0x0020 | class |
ACC_SYNCHRONIZED | 0x0020 | method |
ACC_VOLATILE | 0x0040 | field |
ACC_BRIDGE | 0x0040 | method |
ACC_TRANSIENT | 0x0080 | field |
ACC_VARARGS | 0x0080 | method |
ACC_NATIVE | 0x0100 | method |
ACC_INTERFACE | 0x0200 | class, inner |
ACC_ABSTRACT | 0x0400 | class, inner, method |
ACC_STRICT | 0x0800 | method |
ACC_SYNTHETIC | 0x1000 | class, inner, field, method |
ACC_ANNOTATION | 0x2000 | class, inner |
ACC_ENUM | 0x4000 | class, inner, field |
其中大部分都能见名知意,补充以下几点:
当前类或接口,指向一个CONSTANT_Class_info常量,可以从中解析当前类的全限定名称。包名层次用 /
分割,而不是 .
,如 java/lang/Object
。
当前类的直接父类索引,指向一个CONSTANT_Class_info常量,当没有直接父类时super_class=0
首先用u2表明当前类或接口的直接父接口数量n。紧接着n个u2组成的数组即是这些父接口在常量池的索引,类型是CONSTANT_Class_info,按声明顺序从左至右。
field_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; } 复制代码
field_info保存当前类的fields信息。很简单,其中大部分前面都讲过了,关于attributes放在下文第11节专门讲解。需要注意的是fields只包含当前类的字段,如A的内部类B的字段c,则是在类A$B中
method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; } 复制代码
保存当前类的方法信息,同field_info
属性表:属性存在与 ClassFile
, field_info
, method_info
中,此外Code属性中又包含嵌套属性信息,属性用来描述指令码,异常,注解,泛型等信息,JLS8预定义了23种属性,每种属性结构不同(变长),但可以抽象成下面通用结构。
attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; } 复制代码
attribute_name_index:是该属性名称在常量池中的索引,通过该名称才可以判定当前属性属于具体哪一种,如“Code”表示当前是一个Code_attribute
attribute_length:表示接下来多少字节是该属性的内容信息,java允许自定义新的属性,如果jvm不认识,则按通用结构直接读取attribute_length个字节。
23种属性按作用可以分为3组:
注:后面我会介绍如何解析class,所以本文只对每个属性的结构和作用做一个简单介绍
ConstantValue_attribute { u2 attribute_name_index; u4 attribute_length; u2 constantvalue_index; } 复制代码
存在于field_info,代表一个常量值,如 private final int x = 5
中的 5
。attribute_name_index引用的值是“ConstantValue”,attribute_length固定为2,接下来两个字节的constantvalue_index是该常量值在常量池中的索引,是CONSTANT_Long,CONSTANT_Float,CONSTANT_Double,CONSTANT_Integer,CONSTANT_String的一种。
Code_attribute { u2 attribute_name_index; u4 attribute_length; u2 max_stack; u2 max_locals; u4 code_length; u1 code[code_length]; u2 exception_table_length; { u2 start_pc; u2 end_pc; u2 handler_pc; u2 catch_type; } exception_table[exception_table_length]; u2 attributes_count; attribute_info attributes[attributes_count]; } 复制代码
描述方法体编译后的字节码指令。前面讲过描述方法的 method_info
结构,而方法的方法体信息就存在它的属性表中code属性内。如果是抽象方法,那就没有这个属性。
前面在讲属性通用结构 attribute_info
的时候已经讲过 attribute_name_index
, attribute_length
,它是每个属性都有的,下文就不在说明了,只对其他部分介绍。
max_stack , 操作数栈的最大深度,用来分配栈的大小
max_locals, 方法栈帧中局部变量表最大容量,存储局部变量,方法参数,异常参数等。以slot为单位,32bit以内的变量用分配1个slot,大于32bit,如long、double分配2个slot,注意对象存的是引用。另外指出一点,对于实例方法,默认会传入this对象指针,所以这时的max_locals最小为1。
code[code_length],存储字节码指令列表,每条字节码指令是一个byte,这样8bit最多可以表示256条不同指令,需要指出的是这个字节流数组存的不全是指令,有的指令还有对应的操作数,跳过相应n个字节的操作数再往后才是下一条指令,详细内容我会在另外的文章中演示。
exception_table[exception_table_length],方法异常表,注意不是方法声明抛出的异常,而是显示try-catch的异常,每个catch的异常时exception_table的一项。
这几项表示的意思是:如果在[start_pc, end_pc)区间发生了catch_type类型或其子类的异常(catch_type=0表示捕获任意异常),则跳转至handler_pc处的指令继续执行。
补充三点:
1)关于finaly块中的指令采用的方式是在每个代码分支中冗余一份。
2)关于未显示捕获的异常则通过 athrow
指令继续抛出
3)虽然指令长度code_length是u4,但start_pc,end_pc,handler_pc都只有2个字节的无符号数u2,最大表示范围只有65535,因此方法最多只能有65535条指令(每条指令都不带操作数的情况下)
attributes[attributes_count],嵌套属性列表
StackMapTable_attribute { u2 attribute_name_index; u4 attribute_length; u2 number_of_entries; stack_map_frame entries[number_of_entries]; } 复制代码
上面讲到Code_attribute中也可以包含属性表,StackMapTable就位于Code属性的属性表中,它是为了在jvm字节码验证阶段做类型推导验证而添加的
Exceptions_attribute { u2 attribute_name_index; u4 attribute_length; u2 number_of_exceptions; u2 exception_index_table[number_of_exceptions]; } 复制代码
表示通过 throws
声明的可能抛出的异常,结构很简单exception_index_table每一项u2指向一个CONSTANT_Class_info常量
BootstrapMethods_attribute { u2 attribute_name_index; u4 attribute_length; u2 num_bootstrap_methods; { u2 bootstrap_method_ref; u2 num_bootstrap_arguments; u2 bootstrap_arguments[num_bootstrap_arguments]; } bootstrap_methods[num_bootstrap_methods]; } 复制代码
位于ClassFile中,保存 invokedynamic 指令引用的引导方法
CONSTANT_String_info
, CONSTANT_Class_info
, CONSTANT_Integer_info
, CONSTANT_Long_info
, CONSTANT_Float_info
, CONSTANT_Double_info
, CONSTANT_MethodHandle_info
, or CONSTANT_MethodType_info
引用 InnerClasses_attribute { u2 attribute_name_index; u4 attribute_length; u2 number_of_classes; { u2 inner_class_info_index; u2 outer_class_info_index; u2 inner_name_index; u2 inner_class_access_flags; } classes[number_of_classes]; } 复制代码
记录内部类信息,classes就是当前类的内部类列表,其中inner_class_info_index,outer_class_info_index指向CONSTANT_Class型常量,分别代表内部类和外部类信息引用,inner_name_index是内部类名称的引用(CONSTANT_Utf8_info),等于0则代表是匿名内部类,inner_class_access_flags是内部类访问标志,同access_flags
EnclosingMethod_attribute { u2 attribute_name_index; u4 attribute_length; u2 class_index; u2 method_index; } 复制代码
位于ClassFile结构中,存储局部类或匿名类信息。
Synthetic_attribute { u2 attribute_name_index; u4 attribute_length; } 复制代码
标记是否类、方法、字段为编译器生成,与ACC_SYNTHETIC同义,attribute_length=0,存在该属性则表示true。
Signature_attribute { u2 attribute_name_index; u4 attribute_length; u2 signature_index; } 复制代码
存在于类,方法,字段的属性表中,用于存储类,方法,字段的泛型信息(类型变量Type Variables,参数化类型Parameterized Types)。
关于泛型可以参考这里
RuntimeVisibleAnnotations_attribute { u2 attribute_name_index; u4 attribute_length; u2 num_annotations; annotation annotations[num_annotations]; } 复制代码
存在于类,方法,字段,存储运行时可见的(RetentionPolicy.RUNTIME)注解信息,可以被反射API获取到,关于注解可以参考这里
annotation结构存储了注解名称,元素值对的信息,具体可以参考官方文档,或者我后面class解析的文章
RuntimeInvisibleAnnotations_attribute { u2 attribute_name_index; u4 attribute_length; u2 num_annotations; annotation annotations[num_annotations]; } 复制代码
与RuntimeVisibleAnnotations结构相同,但不可见,即不能被反射API获取到,目前jvm忽略此属性
RuntimeVisibleParameterAnnotations_attribute { u2 attribute_name_index; u4 attribute_length; u1 num_parameters; { u2 num_annotations; annotation annotations[num_annotations]; } parameter_annotations[num_parameters]; } 复制代码
存在于method_info的属性表中,存储运行时可见的方法参数注解信息,与RuntimeVisibleAnnotations对比发现,RuntimeVisibleParameterAnnotations存储的是方法的参数列表上每个参数的注解(相当与一组RuntimeVisibleParameterAnnotations),顺序与方法描述符中参数顺序一致
RuntimeInvisibleParameterAnnotations_attribute { u2 attribute_name_index; u4 attribute_length; u1 num_parameters; { u2 num_annotations; annotation annotations[num_annotations]; } parameter_annotations[num_parameters]; } 复制代码
不想再啰嗦了
RuntimeVisibleTypeAnnotations_attribute { u2 attribute_name_index; u4 attribute_length; u2 num_annotations; type_annotation annotations[num_annotations]; } 复制代码
存在于class_file,method_info,field_info,code的属性表中,java8新增。JLS8新增两种ElementType(ElementType.TYPE_PARAMETER, ElementType.TYPE_USE),相应用来描述的注解属性也做了相应的改的,就有了该属性,type_annotation存储着注解信息及其作用对象。
RuntimeInvisibleTypeAnnotations_attribute { u2 attribute_name_index; u4 attribute_length; u2 num_annotations; type_annotation annotations[num_annotations]; } 复制代码
略。。。
AnnotationDefault_attribute { u2 attribute_name_index; u4 attribute_length; element_value default_value; } 复制代码
存在于 method_info
属性表 ,记录注解元素的默认值
MethodParameters_attribute { u2 attribute_name_index; u4 attribute_length; u1 parameters_count; { u2 name_index; u2 access_flags; } parameters[parameters_count]; } 复制代码
存在于 method_info
属性表 ,记录方法参数信息,name_index形参名称,access_flags有ACC_FINAL,ACC_SYNTHETIC,ACC_MANDATED
SourceFile_attribute { u2 attribute_name_index; u4 attribute_length; u2 sourcefile_index; } 复制代码
class_file属性表中,记录生成该的文件名,异常堆栈可能显示此信息,一般与类名相同,但内部类不是。这是一个可选属性,意味着不强制编译器生成此信息。
SourceDebugExtension_attribute { u2 attribute_name_index; u4 attribute_length; u1 debug_extension[attribute_length]; } 复制代码
存在于class结构中,可选,保存非java语言的扩展调试信息。 debug_extension
数组是指向CONSTAN_Utf8_info的索引
LineNumberTable_attribute { u2 attribute_name_index; u4 attribute_length; u2 line_number_table_length; { u2 start_pc; u2 line_number; } line_number_table[line_number_table_length]; } 复制代码
code的属性表中,存储源码行号与字节码偏移量(方法第几条指令)之间映射关系,start_pc字节码偏移量,line_number源码行号,可选。
问题:在错误堆栈中如何打印出出错的源码行号的?如何支持在源码上断点调试?
LocalVariableTable_attribute { u2 attribute_name_index; u4 attribute_length; u2 local_variable_table_length; { u2 start_pc; u2 length; u2 name_index; u2 descriptor_index; u2 index; } local_variable_table[local_variable_table_length]; } 复制代码
code的属性表中,存储栈帧中局部变量表的变量与源码中定义的变量的映射,可以在解析code属性时关联到局部变量表变量在源码中的变量名等,可选。
LocalVariableTypeTable_attribute { u2 attribute_name_index; u4 attribute_length; u2 local_variable_type_table_length; { u2 start_pc; u2 length; u2 name_index; u2 signature_index; u2 index; } local_variable_type_table[local_variable_type_table_length]; } 复制代码
code的属性表中,与LocalVariableTable相似,signature_index也引用一个 CONSTANT_Utf8_info
常量,对应含有泛型的变量会同时存储到LocalVariableTable和LocalVariableTypeTable中个一份
Deprecated_attribute { u2 attribute_name_index; u4 attribute_length; } 复制代码
类、方法、字段过期标记,没有额外信息,attribute_length=0,如果出现该属性则说明加了@deprecated注解
完! 如果觉得写的还可以,给个赞鼓励一下吧!
下期预告:动手编写一个解析class(字节码)文件的程序
关注微信号,更多精彩等着你