目录
2.1 UTF-8 编码的字符串 CONSTANT_Utf8_info
2.4 字符串字面量 CONSTANT_String_info
2.5 字段引用信息 CONSTANT_Fieldref_info
2.6 方法引用信息 CONSTANT_Methodref_info
2.7 接口方法引用信息 CONSTANT_InterfaceMethodref_info
2.8 名称及类型信息 CONSTANT_NameAndType
4. 类索引(This_class),父类索引(Super_class),接口索引集合(Interfaces)
1. Class文件结构简介
不管是大端模式还是小端模式,cpu在内存的读写数据都是从低地址开始到高地址; 大端模式就是先读到的是高位(低地址存高位,高地址存低位),小端模式就是先读到的是低位(低地址存低位,高地址存高位)。
大端小端产生的根本原因:在计算机中,内存之间不能之间传输数据,需要通过寄存器转,内存的存储单元是字节,但是寄存器是多字节的,于是就有了高位、低位之分,不同的厂家生产的CPU的标准不同,有的用大端(绝大多数),有的用小端,大端小端也就随之而来。
JDK的bin目录下,有个javap的工具,可以在命令行窗口中利用此工具显示出Class文件的结构,便于分析class字节码文件。
class文件结构简介:
class文件的结构就如上图所示,固定顺序,固定格式,以字节为单位,字节之间不存在任何空隙,对于超过8位的数据,将高位放在class文件的前面,低位放在后面(这是大端模式),否则jvm无法正确解析class文件。
magic:魔数,u4表示4个字节,就是一个文件类型的标志,表明此文件是哪种类型,png、TXT,等等,java class文件的魔数固定是0xCAFEBABE,虚拟机加载class文件时,会先检查这个部分,如果不是这个值,虚拟机拒绝加载该文件。
minor_version:次版本号;
major_version:主版本号; 主次版本号构成版本号,class文件的版本号对应着jdk的版本号,比如:45(1.1)、46(1.2)、47(1.3)、48(1.4)、49(1.5)、50(1.6)、51(1.7),可以看到jdk版本每增加0.1,版本号就加1,低版本JDK编译生成的class文件,可以被高版本的JRE执行,但是反之,则不行,虚拟机加载class文件之前,会先查看class文件的版本是否在自己的支持范围之内。
constant_pool_count:常量池的大小,从1开始计数,比如count = 11,那么元素共10个,1~10,第0个单元空出来;
constant_pool:常量池,是数组形式,由字面常量 和 符号引用 组成,保存字面常量和符号引用,符号引用保存的是引用的全局限定名,所以保存的是字符串;
access_flags:当前类的访问权限;
this_class:当前类的全局限定名在常量池中的索引(常量池以数组方法存储,索引即为下标);
super_class:当前类的父类的全局限定名在常量池中的索引;
interfaces_count:当前类实现的接口数目;
interfaces[ interfaces_count ]:这些接口的全局限定名在常量池中的索引的数组;
fields_count:字段的数量;
fields[ fields_count ]:
结构图如下:
重点说明:
用下面这个类的代码作为分析的例子:
2、常量池
常量池主要存放字面量 和 符号引用,符号引用包括:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
常量池的存储形式可理解为数组,每个元素又是一个结构(标识(u1类型,一个字节),元素内容),每个元素都有一个索引值,通过这个索引值就可以定位数组中的某个元素,元素的类型有很多,通过标识位来区分,如下表(可以理解常量池主要存储字面量和符号引用了吧!):
上表是元素类型表,常量池每个元素的具体结构如下表,下表中那些引用结构中,index为索引,索引就是常量池中的数组下标。
好,开始分析Test类的class文件(下面用16进制表示)中的常量池内容:
0700 02 01 00 0454 65 73 74 但看这段,07代表类或者接口的符号引用,index占2字节,值为2,代表这是类,不是接口,接着 01代表字符串,length为04,后面4个字节为字符串内容0x54657374 编码过来就是“Test” 。这句 class Test。
其它的类似这样。
我们一个一个来分析每种类型。
2.1 UTF-8 编码的字符串 CONSTANT_Utf8_info
下图是它的数据结构,第一个是类型标识(1个字节),第二个是这个字符串的长度是多少字节(2个字节),因此变量名、方法名、类名、接口名、字符串常量的最大长度不能超过65535个字节,因为u2能表示的最大数就是65535 bytes = 64KB - 1byte,第三个参数就是字符串的内容了。
2.2 整数 CONSTANT_Integer_info
第一个参数是类型标识(1个字节),如果虚拟机读到类型是3,就知道它是Int类型,默认读取随后的4个字节,第二参数就是int值(4个字节)。
Float、Long、Double类型的都是一个道理。
用于描述类名或者接口名,第一个参数是类型标识(1个字节),第二参数是常量池索引,指向第几个常量项,这个常量项存的就是类名(其实就是utf-8字符串常量来存储类名)。
2.4 字符串字面量 CONSTANT_String_info
用于描述字符串字面量,比如“heheh”,第一个参数是类型标识(1个字节),第二个参数是常量池索引,指向的常量项存储着字符串字面量的全限定名(还是UTF-8字符串常量来存储的)。
2.5 字段引用信息 CONSTANT_Fieldref_info
用于描述类的成员变量的引用信息,第一个参数是类型标识,第二个参数是索引,指向自己的类的CONSTANT_Class_info常量项,第三个参数是索引,指向名称及类型描述符CONSTANT_NameAndType常量项。
2.6 方法引用信息 CONSTANT_Methodref_info
用于描述类的成员方法的引用信息,第一个参数同上,第二参数是索引,指向自己的类的CONSTANT_Class_info常量项,第三个参数是索引,指向名称及类型描述符CONSTANT_NameAndType常量项。
2.7 接口方法引用信息 CONSTANT_InterfaceMethodref_info
用于描述接口的成员方法的引用信息,第一个参数同上,第二参数是索引,指向自己的接口的CONSTANT_Class_info常量项,第三个参数是索引,指向名称及类型描述符CONSTANT_NameAndType常量项。
2.8 名称及类型信息 CONSTANT_NameAndType
用于描述某个内容的名称和类型的信息,第一个参数同上,第二个参数是索引,指向存储该内容的名称的常量项(utf-8常量项),第三个参数是索引,指向该内容的类型描述的常量项(utf-8常量项)。
3、访问标志Access_flag
常量池结束后就是访问标志的内容,2个字节,这个标志用于表示一个类或者接口的访问信息,包括:这个是类还是接口,是否定义为public类型,是否定义为abstract类型,是否被声明为final等,各种标志的值如下:
这些值是16进制表示的,仔细想,占据32位,每一种标志指示占用了其中一个bit(位)。
所有如果这个类或者接口同时又上表中的多个属性,那么只需将相应的bit置为1即可。
4. 类索引(This_class),父类索引(Super_class),接口索引集合(Interfaces)
This_class 是一个2字节的索引,代表本类(接口)的全限定名,指向的是常量池中CONSTANT_Class_info。
Super_class 是一个2字节的索引,代表本类的父类,仅仅是父类,指向常量池中CONSTANT_Class_info,因为java类最多只有1个父类,没有继承某个类的话,默认就是Object类,如果本类是个接口,那么这个Super_class为0,因为接口是不可能有父类的,接口只能有父接口。
Interfaces 是接口集合,用于表示一个类实现了哪些接口,或者一个接口继承了哪些接口,分为两部分:interfaces_count 接口数量(2个字节)、interfaces[ ] 接口数组(存的就是那些接口),如果接口数量为0,那么后面就没有接口数组了。
字段集合,我习惯叫做字段数组,用于描述接口或者类中的变量,包括类级变量或者实例级变量,不包括在方法内声明的变量。可以描述的信息有:变量的作用域(public、private、protected)、是类级变量还是实例级变量(static 修饰符)、可变性(final)、并发可见性(volatile)、可否序列化(transient)、变量数据类型(基本类型、对象、数组)、变量名称。
字段集合中不会列出从超类、父接口中继承来的变量,但是一个内部类的字段集合中会自动添加使用到外部类的那些变量。
fields_count 表示变量的数量,随后就存放多个变量的信息,类似数组一样,变量是有结构的,结构如下表:
access_flags:和字节码文件中的Access_flags非常类似,用于表示一个变量的访问属性(public、protected、private、static等等),如下表:每个标志类型占据32位中的一位,拥有什么访问属性,就将该bit置为1。
name_index:变量的简单名字(不是全局限定名那种,就是名字而已,比如int aa = 5;简单名字就是aa,void ss(){ },简单名字就是ss)的索引,索引就是常量池的下标,变量的简单名字存在常量池;
descriptor_index:变量的描述符的常量池索引,(描述符就是描述变量的数据类型、方法的参数列表、方法的返回值)此处就是变量的数据类型,变量的数据类型也存储在常量池中;这些基本数据类型都用一个大写字符来表示,只有对象类型是用大写字符L加上对象的全限定名加上分号来表示,具体如下表:
数组类型如何表示呢?是几维数组,就在前面加几个“ [ ”符号,比如 double[ ] aaa; 那么在常量池中存的描述符就是 [D, String[ ][ ] bbb;那么常量池中的描述符就是[[Ljava/lang/String。
attributes_count:属性表的属性(额外信息)的数量;
attributes[ attributes_count ]:属性表,每个元素又是一个结构(这个就复杂了,本文没给出详解);
方法集合和字段集合非常类似,基本一样,只是描述符和access_flags的内容不一样,方法的描述符按照参数列表、返回值的顺序描述,方法的参数按照顺序放在“()”里,返回值的类型就放在括号后面,比如 void aaa() 的描述符“()V”,int hehe(char[] aa, String bbb) 的描述符是“([CLjava/lang/String)V”。
Test类中显示定了两个方法,但是class文件中的methods_count =4 ;因为编译器默认为Test类添加了void <clinit>()方法,这个方法是初始化静态变量和静态块的,还添加了一个默认的无参构造函数,所以methods_count = 4。
access_flag的信息如下:
那方法里的java代码在class字节码文件里存在哪里呢?存储在属性表attributes里一个名为“Code”的属性里。
注意:java语言里,对于方法,只有返回值类型不同的话,不能称为重载,会报错,但是在class字节码文件中,只要方法的描述符不相同,这些方法就能共存,换句话说,就是即便两个方法只有返回值类型不同,也能在class字节码文件中共存。
class文件有属性表、字段集合中有属性表、方法集合中有属性表。
java虚拟机规定了有哪些属性,随着java的发展,规定的属性还在增加,虚拟机会忽略掉不认识的属性。下面只给了9个属性:
这些属性就不详解了,只有方法有的属性,在属性表中才会出现。