java代码是经过java编译器编译成class文件,而后由jvm加载执行的,jvm屏蔽了底层平台系统执行细节,因此能够作到Compile Once,Run Anywhere。java
编译后的class文件,是一个二进制流文件,例以下面的类:bootstrap
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语言结构体的形式来描述其逻辑结构,则以下图所示:app
从图中可知,class文件主要包含magic,minor version,major version,constant pool,access flags,this_class,super class,interfaces,fields,methods,attributes 11个部分,每一个部分之间紧凑的拼接在一块儿,没有分界符分割,下面分别介绍每一个结构。jvm
在开始介绍各个结构以前,须要说明本文以jvm1.8为准ide
本文有些长,这里排版看起来更舒服些工具
魔数: 占4个字节的无符号数,固定为0xCAFEBABE,用来标识改文件是一个class文件ui
次版本号: 占两个字节的无符号数,范围0~65535,与major version一块儿表示当前class文件的版本,jvm能够向前兼容以前的版本,但不能向后兼容,即jdk7的虚拟机不能运行jdk8编译的classthis
主版本号: 占两个字节的无符号数,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为例,code在多个方法中都有用到,相比保存多份该field信息来说,在常量池中保存一份该field信息,而后在其余用到的地方保存其索引显然更合适。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
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(字节码)文件的程序
关注微信号,更多精彩等着你