无关性的基石
字节码
Java是一种跨平台的语言,各种不同平台的Java虚拟机,以及所有平台都统一支持的程序存储格式“字节码(Byte Code)”是构成无关性的基石。对于字节码来说,不仅仅是平台的无关性,甚至可以是语言的无关性,我们只需要保证能得到规定格式的字节码文件,无论这个字节码文件是从Java来的,还是从Go来的,我们都不关心。因为只要“不择手段”地得到了字节码文件,我们就可以将字节码文件放到虚拟机上去跑。作为一个通用的、与机械无关的执行平台,任何其它语言的实现都可以将Java虚拟机作为它们语言的允许基础,以Class文件(字节码文件)作为它们产品的交付媒介。虚拟机丝毫不关心字节码文件来源于什么语言,它只关心这个字节码文件是否是我能运行的。
字节码文件简介
字节码文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排序列在文件之中,中间没有添加任何分隔符。这使得整个字节码文件中存储地内容几乎全部都是程序运行地必要数据,没有空袭存在。字节码文件的中:某个字节代表什么含义、长度是多少、先后顺序如何….这些都是被严格限制的,不允许随便改动的。
上述字节码文件采用的是16进制的表示方法。
《Java虚拟机规范》规定Class文件格式采用一种类似C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:“无符号数”和“表”。
- 无符号数:无符号数属于基本的数据类型。以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数。无符号数可以用来描述数字、索引引用、数量值、UTF-8编码的字符串值。
- 表:表是由多个无符号数或其他表作为数据项构成的复合数据类型,所有表的命名一般“_info”结尾。表用于描述层次关系的复合结构的数据,整个Class文件本质上也可以看成一张表,这张表由下表所示的数据项按顺序排列构成。 | 类型 | 名称 | 数量 | | —- | —- | —- | | u4 | magic | 1 | | u2 | minor_version | 1 | | u2 | major_version | 1 | | u2 | constant_pool_count | 1 | | cp_info | constant_pool | constant_pool_count - 1 | | u2 | access_flags | 1 | | u2 | this_class | 1 | | u2 | super_class | 1 | | u2 | interfaces_count | 1 | | u2 | interfaces | interfaces_count | | u2 | fields_count | 1 | | field_info | fields | fields_count | | u2 | methods_count | 1 | | method_info | methods | methods_count | | u2 | attribute_count | 1 | | attribute_info | attributes | attributes_coun |
Class类文件结构详解
Class类文件总体认知
下面这张图很好的表示了字节码文件的结构:
这张图和上面那张表也是一一对应的。
Class类文件结构详细认识
关于详细认识,由于全部记住是十分困难的,只需要大概了解即可。
class文件中的魔数和版本号
魔数(magic)
在class文件开头的四个字节, 存放着class文件的魔数, 这个魔数是class文件的标志,他是一个固定的值: 0XCAFEBABE 。 也就是说他是判断一个文件是不是class格式的文件的标准, 如果开头四个字节不是0XCAFEBABE, 那么就说明它不是class文件, 不能被JVM识别。
版本号(minor_version 和 major_version)
紧接着魔数的四个字节是class文件的次版本号和主版本号。 随着Java的发展,class文件的格式也会做相应的变动。版本号标志着class文件在什么时候,加入或改变了哪些特性。举例来说,不同版本的javac编译器编译的class文件,版本号可能不同, 而不同版本的JVM能识别的class文件的版本号也可能不同,一般情况下, 高版本的JVM能识别低版本的javac编译器编译的class文件,而低版本的JVM不能识别高版本的javac编译器编译的class文件。如果使用低版本的JVM执行高版本的class文件, JVM会抛出java.lang.UnsupportedClassVersionError 。
class文件中的常量池计数器和常量池
常量池计数器(constant_pool_count)
常量池是class文件中非常重要的结构,它描述着整个class文件的字面量信息。 常量池是由一constant_pool结构体数组组成的,而数组的大小则由常量池计数器指定。常量池计数constant_pool_count =constant_pool表中的成员数+ 1。constant_pool表的索引值只有在大于 0 且小于constant_pool_count时才会被认为是有效的。
注意:
- 常量池计数器是从1开始计数的,而不是0。如果常量池计数器数值为n,则后面常量池项(cp_info)的个数是n-1,而不是n。
- 第0项空出来的原因是为了满足某些指向常量池的索引值的数据在特定情况下表达“不引用任何一个常量池项”的意思,这种情况可以将索引值设置为0来表示
常量池(constant_pool)
常量池是class文件中的一项非常重要的数据。 常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
- 字面量比较接近于Java语言层面的常量概念,如文本字符串、 声明为final的常量值等。
- 符号引用则属于编译原理方面的概念,包括了下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
下面依次介绍这三种:
类和接口的全限定名
在Java源文件中的全限定名是java.lang.Object , 而class文件中的全限定名是将点号替换成“/” 。 例如, Object类在class文件中的全限定名是 java/lang/Object 。
字段的名称和描述符
基本数据类型和void类型 | 类型的对应字符 |
---|---|
byte | B |
char | C |
double | D |
float | F |
int | I |
long | J |
short | S |
boolean | Z |
void | V |
引用类型(类和接口,枚举):“L” + 类型的全限定名 + “;”
如:Object在描述符中的对应字符串是: Ljava/lang/Object;ArrayList在描述符中的对应字符串是: Ljava/lang/ArrayList; 自定义类型com.example.Person在描述符中的对应字符串是: Lcom/example/Perso
数组类型:若干个“[” + 数组中元素类型的对应字符串
如:int[]类型的对应字符串是: [I ;int[][]类型的对应字符串是: [[I ;Object[]类型的对应字符串是: [Ljava/lang/Object ;Object[][][]类型的对应字符串是: [[[Ljava/lang/Object。
方法的名称和描述符
(参数1类型 参数2类型 参数3类型 …)返回值类型
特殊方法的方法名,如类的构造方法的方法名使用字符串
常量池中几乎包含类中的所有信息的描述, class文件中的很多其他部分都是对常量池中的数据项的引用,比如后面要将的this_class, super_class, field_info, attribute_info等,另外字节码指令中也存在对常量池的引用, 这个对常量池的引用当做字节码指令的一个操作数。此外,常量池中各个项也会相互引用。
类型和类型标志的对应如下表所示:
常量池中数据项类型 | 类型标志 | 类型描述 |
---|---|---|
CONSTANT_Utf8 | 1 | UTF-8编码的Unicode字符串 |
CONSTANT_Integer | 3 | int类型字面值 |
CONSTANT_Float | 4 | float类型字面值 |
CONSTANT_Long | 5 | long类型字面值 |
CONSTANT_Double | 6 | double类型字面值 |
CONSTANT_Class | 7 | 对一个类或接口的符号引用 |
CONSTANT_String | 8 | String类型字面值 |
CONSTANT_Fieldref | 9 | 对一个字段的符号引用 |
CONSTANT_Methodref | 10 | 对一个类中声明的方法的符号引用 |
CONSTANT_InterfaceMethodref | 11 | 对一个接口中声明的方法的符号引用 |
CONSTANT_NameAndType | 12 | 对一个字段或方法的部分符号引用 |
CONSTANT_MethodHandle | 15 | 表示方法句柄 |
CONSTANT_MethodType | 16 | 表示方法类型 |
CONSTANT_InvokeDynamic | 18 | 表示用invokedynamic指令所使用的引导方法(Bootstrap Method),引导方法使用动态调用名称(Dynamic Invocation Name),参数和请求返回类型以及可以选择性的附加被称为静态参数(static arguments) 的常量序列。 |
class文件中的访问标志(access_flags)
访问标志(access_flags)紧接着常量池后,占有两个字节,总共16位。
class文件中的this_class
一个Java类源文件经过JVM编译会生成一个class文件,也有可能一个Java类源文件中定义了其他类或者内部类,这样编译出来的class文件就不止一个,但每一个class文件表示某一个类,至于这个class表示哪一个类,便可以通过类索引这个数据项来确定。
class文件中的super_class
Java支持单继承模式,除了java.lang.Object 类除外,每一个类都会有且只有一个父类。class文件中紧接着类索引(this_class)之后的两个字节区域表示父类索引,跟类索引一样,父类索引这两个字节中的值指向了常量池中的某个常量池项CONSTANT_Class_info,表示该class表示的类是继承自哪一个类。如果没有显式的继承一个,也就是说如果当前类是直接继承Object的, 那么super_class值为0 。 如果一个索引值为0, 那么就说明这个索引不引用任何常量池中的数据项, 因为常量池中的数据项是从1开始的。 也就是说, 如果一个类的class文件中的super_class为0 , 那么就代表该类直接继承Object类。
class文件中的interfaces_count和interfaces
紧接着super_class的是interfaces_count, 表示当前类所实现的接口的数量或者当前接口所继承的超接口的数量。 注意, 只有当前类直接实现的接口才会被统计, 如果当前类继承了另一个类, 而另一个类又实现了一个接口, 那么这个接口不会统计在当前类的interfaces_count中。 在interfaces_count后面是interfaces, 他可以看做是一个数组, 其中的每个数组项是一个索引, 指向常量池中的一个CONSTANT_Class_info, 这个CONSTANT_Class_info又会引用常量池中的一个CONSTANT_Utf8_info , 这个CONSTANT_Utf8_info 中存放着有当前类型直接实现或继承的接口的全限定名。 当前类型实现或继承了几个接口, 在interfaces数组中就会有几个数项与之相对应。
class文件中的fields_count和fields
字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。
Java中的一个Field字段应该包含信息如下:
FIeld_info的组成元素:访问标志(access_flags)、名称索引(name_index)、描述索引(descriptor_index)、属性表集合其中access_flags占两个字节, 描述的是字段的访问标志信息。
class文件中的methods_count和methods
方法表集合是指由若干个方法表(method_info)组成的集合。对于在类中定义的若干个,经过JVM编译成class文件后,会将相应的method方法信息组织到一个叫做方法表集合的结构中,字段表集合是一个类数组结构,如下图所示:
methods_count描述的是当前的类中定义的方法的个数,这里包括静态方法, 但不包括从父类继承的方法。 如果当前class文件是由一个接口生成的, 那么这里的methods_count描述的是接口中定义的抽象方法的数量, 我们知道, 接口中定义的方法默认都是公有的。此外需要说明的是, 编译器可能会在编译时向class文件增加额外的方法, 也就是说, class文件中的方法的数量可能多于源文件中由用户定义的方法。 举例来说: 如果当前类没有定义构造方法, 那么编译器会增加一个无参数的构造函数; 如果当前类或接口中定义了静态变量, 并且使用初始化表达式为其赋值, 或者定义了static静态代码块, 那么编译器在编译的时候会默认增加一个静态初始化方法 。
位于methods_count下面的数据叫做methods , 可以把它看做一个数组, 数组中的每一项是一个method_info 。这个数组中一共有methods_count个method_info , 每个method_info 都是对一个方法的描述。
上图中的method_info结构体的定义,该结构体的定义跟描述field字段 的field_info结构体的结构几乎完全一致方法表的结构体由:访问标志(access_flags)、名称索引(name_index)、描述索引(descriptor_index)、属性表(attribute_info)集合组成。
method_info结构体最前面的两个字节表示的访问标志(access_flags),记录这这个方法的作用域、静态or非静态、可变性、是否可同步、是否本地方法、是否抽象等信息。
名称索引(name_index):
紧跟在访问标志(access_flags)后面的两个字节称为名称索引,这两个字节中的值指向了常量池中的某一个常量池项,这个方法的名称以UTF-8格式的字符串存储在这个常量池项中。
描述索引(descriptor_index):
描述索引表示的是这个方法的特征或者说是签名,一个方法会有若干个参数和返回值,而若干个参数的数据类型和返回值的数据类型构成了这个方法的描述,其基本格式为:(参数数据类型描述列表)返回值数据类型。所谓的方法描述符,实质上就是指用一个什么样的字符串来描述一个方法
class文件中的attribute_count和attribute_info
attribute_count
attribute_info
这个属性表集合非常重要,方法的实现被JVM编译成JVM的机器码指令,机器码指令就存放在一个Code类型的属性表中;如果方法声明要抛出异常,那么异常信息会在一个Exceptions类型的属性表中予以展现。这些信息包括:这个方法的代码实现,即方法的可执行的机器指令;这个方法声明的要抛出的异常信息;这个方法是否被@deprecated注解表示;这个方法是否是编译器自动生成的。
属性表之Code类型
Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。 Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性。 Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、 字段、 方法定义及其他信息)两部分,那么在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。
123456789101112 | 1. attribute_name_index,属性名称索引,占有2个字节,其内的值指向了常量池中的某一项,该项表示字符串“Code”;2. attribute_length,属性长度,占有 4个字节,其内的值表示后面有多少个字节是属于此Code属性表的;3. max_stack,操作数栈深度的最大值,占有 2 个字节,在方法执行的任意时刻,操作数栈都不应该超过这个值,虚拟机的运行的时候,会根据这个值来设置该方法对应的栈帧(Stack Frame)中的操作数栈的深度;4. max_locals,最大局部变量数目,占有 2个字节,其内的值表示局部变量表所需要的存储空间大小;5. code_length,机器指令长度,占有 4 个字节,表示跟在其后的多少个字节表示的是机器指令;6. code,机器指令区域,该区域占有的字节数目由 code_length中的值决定。JVM最底层的要执行的机器指令就存储在这里;7. exception_table_length,显式异常表长度,占有2个字节,如果在方法代码中出现了try{} catch()形式的结构,该值不会为空,紧跟其后会跟着若干个exception_table结构体,以表示异常捕获情况;8. exception_table,显式异常表,占有8 个字节,start_pc,end_pc,handler_pc中的值都表示的是PC计数器中的指令地址。exception_table表示的意思是:如果字节码从第start_pc行到第end_pc行之间出现了catch_type所描述的异常类型,那么将跳转到handler_pc行继续处理。9. attribute_count,属性计数器,占有 2 个字节,表示Code属性表的其他属性的数目。10. attribute_info,表示Code属性表具有的属性表,它主要分为两个类型的属性表:“LineNumberTable”类型和“LocalVariableTable”类型。 “LineNumberTable”类型的属性表记录着Java源码和机器指令之间的对应关系; “LocalVariableTable”类型的属性表记录着局部变量描述; |
---|---|
机器指令(code):JVM使用一个字节表示机器操作码,即对JVM底层而言,它能表示的机器操作码不多于2的 8 次方,即 256个。class文件中的机器指令部分是class文件中最重要的部分。
异常处理跳转信息:如果代码中出现了try{}catch{}块,那么try{}块内的机器指令的地址范围记录下来,并且记录对应的catch{}块中的起始机器指令地址,当运行时在try块中有异常抛出的话,JVM会将catch{}块对应懂得其实机器指令地址传递给PC寄存器,从而实现指令跳转。一个异常处理器(exception_info)的意思是: 如果偏移量从start_pc到end_pc之间的字节码出现了catch_type描述的类型的异常, 那么就跳转到偏移量为handler_pc的字节码处去执行。如果catch_type为0, 就代表不引用任何常量池项(常量池中的项是从1开始计的), 那么这个exception_info用于实现finally子句。
exception_info中的各个字段:
start_pc是从字节码(Code属性中的code部分)起始处到当前异常处理器起始处的偏移量。
end_pc是从字节码起始处到当前异常处理器末尾的偏移量。
handler_pc是指当前异常处理器用来处理异常(即catch块)的第一条指令相对于字节码开始处的偏移量。catch_type是一个常量池索引, 指向常量池中的一个CONSTANT_Class_info数据项, 该数据项描述了catch块中的异常的类型信息。这个类型必须是java.lang.Throwable的或其子类。
Java源码行号和机器指令的对应关系—LineNumberTable属性表:编译器在将java源码编译成class文件时,会将源码中的语句行号跟编译好的机器指令关联起来,这样的class文件加载到内存中并运行时,如果抛出异常,JVM可以根据这个对应关系,抛出异常信息,告诉我们的源码的多少行有问题,方便我们定位问题。这个信息不是运行时必不可少的信息,但是默认情况下,编译器会生成这一项信息,如果你项取消这一信息,你可以使用-g:none 或-g:lines来取消或者要求设置这一项信息。如果使用了-g:none来生成class文件,class文件中将不会有LineNumberTable属性表,造成的影响就是 将来如果代码报错,将无法定位错误信息报错的行,并且如果项调试代码,将不能在此类中打断点(因为没有指定行号。
局部变量表描述信息—-LocalVariableTable属性表:局部变量表信息会记录栈帧局部变量表中的变量和java源码中定义的变量之间的关系,这个信息不是运行时必须的属性,默认情况下不会生成到class文件中。你可以根据javac指令的-g:none或者-g:vars选项来取消或者设置这一项信息。它有什么作用呢? 当我们使用IDE进行开发时,最喜欢的莫过于它们的代码提示功能了。如果在项目中引用到了第三方的jar包,而第三方的包中的class文件中有无LocalVariableTable属性表的区别如下:
Exceptions类型
Exceptions类型的属性表(attribute_info)结构体由一下元素组成:属性名称索引(attribute_name_index):占有 2个字节,其中的值指向了常量池中的表示”Exceptions”字符串的常量池项;属性长度(attribute_length):它比较特殊,占有4个字节,它的值表示跟在其后面多少个字节表示异常信息;异常数量(number_of_exceptions):占有2 个字节,它的值表示方法声明抛出了多少个异常,即表示跟在其后有多少个异常名称索引;异常名称索引(exceptions_index_table):占有2个字节,它的值指向了常量池中的某一项,该项是一个CONSTANT_Class_info类型的项,表示这个异常的完全限定名称;
如果某个方法定义中,没有声明抛出异常,那么,表示该方法的方法表(method_info)结构体中的属性表集合中不会有Exceptions类型的属性表;换句话说,如果方法声明了要抛出的异常,方法表(method_info)结构体中的属性表集合中必然会有Exceptions类型的属性表,并且该属性表中的异常数量不小于1。
我们假设异常数量中的值为 N,那么后面的异常名称索引的数量就为N,它们总共占有的字节数为N2,而异常数量占有2个字节,那么将有下面的这个关系式:
属性长度(attribute_length)中的值= 2 + 2异常数量(number_of_exceptions)中的值 Exceptions类型的属性表(attribute_info)的长度=2 + 4 + 属性长度(attribute_length)中的值
Synthetic类型
Synthetic属性可以出现在filed_info中, method_info中和顶层的ClassFile中, 分别表示这个字段, 方法或类不是有用户代码生成的(即不存在与源文件中), 而是由编译器自动添加的。 例如, 编译器会为内部类增加一个字段, 该字段是对外部类对象的引用; 如果一个不定义构造方法, 那么编译器会自动添加一个无参数的构造方法, 如果定义了静态字段或静态代码块, 还会根据具体情况, 增加静态初始化方法 。 此外, 有些机制, 如动态代理, 会在运行时自动生成字节码文件, 由于这些类不是由源文件中编译来的, 所以这些类的class文件中会有一个Synthetic属性。
Deprecated类型
Deprecated属性可以存在于filed_info中, method_info中和顶层的ClassFile中, 分别表示这个字段, 方法或类已经过时。 这个属性用来支持源文件中的@deprecated注解。 也就是说, 如果在源文件中为一个字段, 方法或类标注了@deprecated注解, 那么编译器就会在class文件中为这个字段, 方法或类生成一个Deprecated属性 。attribute_length永远为0 , 因为这个属性只是一个标志信息, 用来表示字段, 方法, 类已经过时, 而不具有任何实质性的属性信息。
参考文章: http://luckylau.tech/2017/05/26/%E4%BD%A0%E6%87%82java%E5%90%97-11/