一、代码示例
后面的代码举例都已如下代码示例
package org.fenixsoft.clazz; public class TestClass { private int m; public int inc() { return m + 1; } }
编译后的class文件,使用16进制文本打开(我使用的是UItraEdit),内容如下:
二、class文件结构简述
class文件由魔数、版本号(副版本号、主版本号)、常量池信息(常量池计数器和常量池数据区)、访问标志、类索引(当前类索引、父类索引)、接口数据区(接口数量、接口信息)、方法数据区(方法数量、方法信息)、属性数据区(属性数量、属性信息组成)组成。
具体的内容如下表所示,无符号数使用u1、u2、u4、u8分别代表1、2、4、8个字节,如果有多个无符号组成的表,则以 _info 结尾表示(有符号可以类比为java中的简单基本数据类型,而有多个无符号组成的表可以类比为java中的一个对象,对象中都是基本数据类型的属性)
结构如下所示:
三、魔数与版本信息
1、魔数:由上面的表格可以看到,魔数占用4个字节,其值固定为CAFEBABY,与示例中的值一样
2、副版本号(次版本号):占用2个字节,目前没有使用,为 00 00
3、主版本号:占用两个字节。JAVA的其实版本号为45,即JDK1的版本号为45,每一个大版本号加一;高版本jdk可以运行低版本jdk编译的class文件,反之则不可以。以上面的示例代码为例,其版本为十六进制的0x0034,即十进制的52,那么编译class文件的jdk版本则为jkd8(52-45+1)
四、常量池数据区
(一)常量池存储信息
紧跟着主次版本号后面的就是常量池信息。
常量值中主要存放两类信息:字面量和符号引用。
字面量比较接近java中常量的意思,如文本字符串、被final修饰的常量等;
而符号引用则属于编译原理方面的概念,主要包括:
1、被模块到处或者开放的包
2、类和接口的全限定名
3、字段的名称和描述符
4、方法句柄和方法类型
5、动态调用点和动态常量
最早的常量表中有11种结构不相同的表结构数据,后来为了支持动态语言调用,加入了4种,后来又为了支持模块化,又加入了两种,因此至JDK13,共有17种表结构数据。
每一种表结构数据最少有两部分组成,tag和info,其中tag是用来标记不同表结构的,info用来表示具体的信息。具体的info信息,可以看最后的常量表结构。
(二)int和float常量存储结构
首先对于对于基本数据类型和String的值对应的info信息是具体的值,例如int和float的结构如下所示:tag分别为3和4,然后使用4个字节来表示值。
(三)long和double常量存储结构
对于long和double类型,tag分别为5和6,并且使用8个字节存储其值,其中前4个字节为高位数,后4个字节为低位数。
(四)String常量存储结构
对于String类型的数据,tag为8,info为两个字节的字符串索引项(也就是下面要说的utf8格式的cp_info)
(五)utf8常量存储结构
上面String的info信息即是对一个UTF8格式索引项,那么utf8格式的常量,tag为1,然后使用两个字节存储utf8编码字节数组的长度,然后使用具体的长度的数组存储数据。
对于String存储具体是怎么样的,可以参照下图,就是定义了一个String,其字符串为“JVM原理”,那么JVM原理是存储在utf8格式的cp_info中,在utf8的cp_info中,存储了tag,长度,及具体的字符串内容;然后在String的cp_info中,只是存储了对于utf8的cp_info的索引项。
(六)类存储结构
而对于类、方法、属性的cp_info则是由类或接口的索引项和类型名称的索引项。
以类为例,其tag为7,index为占用两个字节的类名称索引项,对应的也是一个utf格式的cp_info
常量表详情:
常量名 | 描述 | 项目(无符号数) | 类型 | 描述 |
CONSTANT_Utf8_info | utf8编码的字符串 | tag | u1 | 值为1 |
length | u2 | utf8的字符串占用了多少字节数 | ||
bytes | u1 | 长度为length的utf8编码的字符串 | ||
CONSTANT_Integer_info | 整型字面量 | tag | u1 | 值为3 |
bytes | u4 | 按照高位在前存储的int值 | ||
CONSTANT_Float_info | 浮点数字面量 | tag | u1 | 值为4 |
bytes | u4 | 按照高位在前存储的float值 | ||
CONSTANT_Long_info | 长整型字面量 | tag | u1 | 值为5 |
bytes | u8 | 按照高位在前存储的long值 | ||
CONSTANT_Double_info | 双精度浮点型字面量 | tag | u1 | 值为6 |
bytes | u8 | 按照高位在前存储的double值 | ||
CONSTANT_Class_info | 类或接口的符号引用 | tag | u1 | 值为7 |
index | u2 | 指向全限定名常量项的索引 | ||
CONSTANT_String_info | 字符串类型字面量 | tag | u1 | 值为8 |
index | u2 | 指向字符串字面量的索引 | ||
CONSTANT_Fieldref_info | 字段的符号引用 | tag | u1 | 值为9 |
index | u2 | 指向字段声明类或接口描述符CONSTANT_Class_info的索引 | ||
index | u2 | 指向字段描述符CONSTANT_NameAndType的索引 | ||
CONSTANT_Methodref_info | 类中方法的符号引用 | tag | u1 | 值为10 |
index | u2 | 指向方法声明类描述符CONSTANT_Class_info的索引。 |
||
index | u2 | 指向名称及类型描述符CONSTANT_NameAndType的索引 | ||
CONSTANT_InterfaceMethodref_info | 接口中方法的引用 | tag | u1 | 值为11 |
index | u2 | 指向方法声明接口描述符CONSTANT_Class_info的索引 | ||
index | u2 | 指向名称及类型描述符CONSTANT_NameAndType的索引 | ||
CONSTANT_NameAndType_info | 字段或方法的部分符号引用 | tag | u1 | 值为12 |
index | u2 | 指向该字段或方法名称常量项的索引 | ||
index | u2 | 指向该字段或方法描述符常量项的索引 | ||
CONSTANT_MethodHandle_info | 表示方法句柄 | tag | u1 | 值为15 |
reference_kind | u1 | 值必须在1-9之间,闭区间,它决定了方法句柄的类型,方法句柄类型值表示方法句柄的字节码行为 | ||
reference_index | u2 | 值必须是对常量池的索引 | ||
CONSTANT_MethodType_info | 表示方法类型 | tag | u1 | 值为16 |
descroptor_index | u2 | 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info | ||
CONSTANT_Dynamic_info | 表示一个动态计算常量 | tag | u1 | 值为17 |
bootstrap_method_attrindex | u2 | 值必须是当前class文件中引导方法表的bootstrap_method[]数组的有效索引 | ||
name_and_type_index | u2 | 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符 | ||
CONSTANT_InvokeDynamic_info | 表示一个动态方法调用点 | tag | u1 | 值为18 |
bootstrap_method_attrindex | u2 | 值必须是当前class文件中引导方法表的bootstrap_method[]数组的有效索引 | ||
name_and_type_index | u2 | 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符 | ||
CONSTANT_Module_info | 表示一个模块 | tag | u1 | 值为19 |
name_index | u2 | 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示模块名称 | ||
CONSTANT_Package_info | 表示一个模块中开放或者导出的包 | tag | u1 | 值为20 |
name_index | u2 | 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示模块名称 |
(七)哪些字面量会进入常量池
结论:
1、final修饰的8种基本数据类型都会进入常量池
2、非final修饰的8种基本数据类型,只有double、long、float的值会进入常量池。
3、常量池种包含的字符串类型字面量(双引号引起来的字符串值)
五、访问标志和类索引(当前类索引、父类索引)
1、访问标志
在常量池结束以后,紧接着两个字节表示访问标志,这个标志用于识别一些类或者接口层次的访问信息,例如是否为public、是否为abstract、如果是类的话是否被声明为final等等,具体标志位及含义如下所示
2、类索引、父类索引、接口集合
类索引和父类索引都是一个u2类型的数据,接口索引集合是一组u2类型的数据集合,class文件中由这三项数据来确定该类型的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类父类的全限定名。接口索引集合用来描述这个类实现了哪些接口。
六、字段表集合和放发表集合
(一)字段表集合
字段表集合用于描述接口或者类中声明的变量。其包含类变量(static修饰)和实例变量。
字段可以包括的修饰符有字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。
字段修饰符放在access_flags项目中,它与类中的access_flags项目是非常类似的,都是一个u2的数据类型。
跟随access_flags标志的是两项索引值:name_index和descriptor_index。它们都是对常量池项的引用,分别代表着字段的简单名称以及字段和方法的描述符。
基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示
举例说明
(二)方法表集合
这里跟上面的字段表很类似,就不再多说,直接上表
(三)属性表集合
属性表(attribute_info),Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。
七、javap命令
可以使用javap命令反编译class文件,例如使用javap命令反编译示例的class文件:javap -v TestClass
从输出结果看,首先是警告,然后是文件地址、最后修改时间、文件大小、MD5加密、包名、次版本号、主版本号、访问标志等内容。
然后就是常量池内容:
1、第一个内容是方法的cp_info,备注里面可以看到,这个方法名是init方法,其有两个索引项,方法对应的类或接口和方法的NameAndType,其分别是4和15
先来看4,说明其是一个类,从备注可以看到该类是Object类,其对应18的索引项,指向了Object类;
再来看15,其是方法的NameAndType,分别对应7和8,7说明该方法名称是init方法,8说明该方法返回值为void
2、第二个内容是字段的cp_info,存在对应类或接口的索引项和字段的NameAndType,分别对应3和16
先来看3,其是一个类,有全限定名的索引项,指向了17,再看17,其类为TestClass类。
再来看16,是一个NameAndType的索引项,其中包含了名称和类型的索引项,对应5和6,5说明了该字段名称为m,6说明了该字段的类型是int
3、再往下就是code区,及code区中方法信息。
4、然后看具体的init方法,其标识了方法的返回值、访问标志、栈深等信息,然后就是具体的操作,使用aload将1压进栈,再使用getfield将获取m,然后使用iconst将m压进栈,然后使用iadd相加,最后使用ireturn返回结果。