盐城市网站,网站空间速度快,公路水运建设质量与安全监督系统网站,靖江做网站哪家好本文将揭开Java Class文件的神秘面纱#xff0c;带你了解Class文件的内部结构#xff0c;并从Class文件结构的视角告诉你#xff1a;
为什么Java Class字节码文件可以“写一次#xff0c;遍地跑”#xff1f;为什么常量池的计数从1开始#xff0c;而不是和java等绝大多数…本文将揭开Java Class文件的神秘面纱带你了解Class文件的内部结构并从Class文件结构的视角告诉你
为什么Java Class字节码文件可以“写一次遍地跑”为什么常量池的计数从1开始而不是和java等绝大多数语言的习惯一样从0开始计数为什么在Java应用运行期间无法使用反射在普通对象中获取到泛型信息
平台无关性
Java应用之所以能“Write once Run anywhere”是因为有JVM虚拟机这个中间媒介来执行Java程序而JVM虚拟机不和包括Java在内的任何语言绑定它只和“Class”文件这种约定的二进制文件格式所关联。虚拟机通过载入和执行平台无关的字节码从而实现了程序的“Write once Run anywhere”。 什么是Class文件
“深入理解Java虚拟机”一书中给出了定义“Class文件是一组以8位字节为基础单位的二进制流”。各个数据项目按照顺序紧凑排列中间没有分隔符整个Class文件没有一点空间上的浪费。
利用idea插件BinEd打开Class文件我们可以看到用十六进制表示的Class文件开头是固定的0xCAFEBABE咖啡宝贝魔数它的唯一作用是用来验证此文件是可以被虚拟机接受的Class文件而不是通过后缀.class来验证因为后缀名是可以人为修改的。很多格式如gif或者jpeg等文件头都存在魔数。 Class文件格式
Class文件格式按照虚拟机规范的约定采用一种类似于C语言结构体的伪结构来存储数据只要各个平台编译器能够严格遵守Java虚拟机的规范来生成Class文件Java虚拟机就能生成它可执行的Class文件。在Class文件中只有两种数据类型
无符号数。属于基本的数据类型u1、u2、u4和u8分别表示1个、2个、4个和8个字节的无符号数它们可以用来描述数字、索引引用、数量值或者utf-8编码的字符串。表。是由多个无符号数或其他表构成的复合数据结构。表习惯以“_info”结尾整个Class文件本质上也是一张表因为它也是具有层次关系的复合数据类型。 如上图当同一个数据类型有多个时经常会在这个数据类型集合的前面加上一个计数器。例如fields数据项表示Class文件里的多个field_info字段其前面的fields_count保存了fields集合的数量。
下面正式开始介绍Class文件的各个数据项的含义和作用。
Class文件的版本
前面已经介绍了Class文件以魔数magic开头魔数是u4类型占用了4个字节。紧随其后的第5和第6个字节是次版本号Minor Version第7和第8个字节是主版本号Major Version。Java的版本号从45开始从1.1开始每个JDK大版本的主版本号都向上加1JDK 1.01.1使用了45.045.3的版本号。例如我的本地JDK版本是1.8所以编译的Class文件大版本号是十六进制0x0034即十进制52。 版本号的作用是保证高版本的JDK向下兼容低版本的Class文件但必须拒绝超过其版本号的Class文件。
常量池
主版本号之后是常量池入口常量池是占用Class文件空间最大的数据项目之一因为其他项目里存放的引用类型是和常量池里存放的数据进行的关联。例如类索引this_class是u2类型的数据里面存放的是引用常量池的地址常量池对应的区域保存的是类的全限定名。
我们先用一个示例来大致的了解一下常量池的结构。以下是class文件对应的源代码我们可以使用idea插件jclasslib打开这个类的class文件看看它的构造。
public class GuoClassT {private int money;public int make() {return money 1000000000;}
}
在一般信息里我们看到有一项“本类索引”this_classcp_info表示本类索引的数据类型是常量池类型constant_pool_info编号#4表示本类索引指向的是常量池的4号位置它存放的数据是CONSTANT_Class_info类型。 点开常量池编号#4的索引可以看到它存放的也是一个cp_info的数据显然我们可以再次点击它跳转到常量池编号25的位置看看。 果然这里我们看到了常量池编号#25的位置存放的是一个字符串字面量它保存的就是我们最终要找的类的全限定名“com/examples/test/GuoClass”。 通过上面的示例我们可以分析出几个重点
常量池的容量计数是从1开始算起的。这和绝大多数语言习惯包括Java都不太一样。这样做的目的在于满足特定情况下不引用常量池项目时将索引值置为0。常量池的常量数量是不固定的。所以在常量池的前面需要放一个类型为u2的数据代表常量池的容量。上面例子中constant_pool_count存放了十六进制数0x001B即十进制27代表常量池有27-1 26个常量索引值范围是1~26。 常量池中主要存放两大类常量字面量Literal和符号引用Symbolic References。字面量类似于Java语言的文本或final常量值。符号引用包括类和接口的全限定名、字段的名称和描述符以及方法的名称和描述符。常量池是最繁琐的数据其中每一项常量都是一个表除了第一位是一个u1类型的标志位tag以外每一项常量均有各自的结构。如图是常量池的项目类型。 常量池每一项数据类型依靠标志位tag来区分。知道了常量池数据项的类型之后就可以根据常量项的结构总表来查询常量项的具体信息。 我们再来看一个例子从总体上直观感受一下常量池的结构。还是解析上面的GuoClass.class类文件图中红框1圈出来的是常量池的第一位的数据项对应的是红框2圈出来的部分它的数据类型是CONSTANT_Methodref_info通过上图我们知道
CONSTANT_Methodref_info的第一部分是一个u1类型的标志位tag红框1中的0x0A即十进制10正好对应标志位tag的枚举值CONSTANT_Methodref_info。
CONSTANT_Methodref_info的第二部分是一个u2类型的索引项红框1中的0x0005即十进制5正好对应红框3中的cp_info#5。
CONSTANT_Methodref_info的第三部分是一个u2类型的索引项红框1中的0x0017即十进制23正好对应红框3中的cp_info#23。 访问标志
如果你能看到这里说明你已经跨过了学习Class文件格式最困难的部分坚持下去一定会有收获
紧接着常量池的是访问标志access_flags它用于识别类或接口层次的访问信息比如这个Class是类还是接口是否为public类型有没有abstract修饰有没有final关键字等等。具体标志位如图所示 以GuoClass这个类为例它是一个普通的Java类不是接口、枚举或者注解public类型没有final和abstract关键字修饰所以它的ACC_PUBLIC、ACC_SUPER标志应当为真其他的标志位应该为假所以它的access_flags的值应为0x0001 | 0x00200x0021。从它的十六进制图可以看出我们的结果是正确的。 类索引、父类索引与接口索引集合
类索引、父类索引和接口索引集合都按顺序排列在访问标志之后。
类索引this_classu2类型数据用于确定类的全限定名。父类索引super_classu2类型数据用于确定类的父类的全限定名。由于Java语言不允许多重继承因此父类索引只能有一个。除了java.lang.Object之外所有的Java类都有父类所以除了java.lang.Object之外所有的Java类的父类索引都不为0。接口索引集合interfaces一组u2类型数据用来描述这个类实现的接口。也就是这个类按implements语句后的接口顺序排列的集合。如果当前类是一个接口则应当是extends语句后的接口。
类索引、父类索引和接口索引的查找过程是一样的都是用u2类型的索引值表示指向一个CONSTANT_Class_info类型的类描述符常量再通过CONSTANT_Class_info类型的常量中的索引值找到CONSTANT_Utf8_info类型的全限定名字符串。 以GuoClass这个类为例在access_flags之后的两个字节是this_class这个u2类型的数据项用十六进制表示为0x0004它指向常量池中第4个类型为CONSTANT_Class_info的常量再根据此常量里的索引值找到常量池中第25个位置保存的CONSTANT_Utf8_info类型的字符串这个字符串就是我们需要找的全限定名“com/examples/test/GuoClass”。 在this_class之后即0x0004后面的四个字节0x0005和0x0000分别表示super_class和interfaces集合的入口。super_class和this_class的查找过程一摸一样而由于GuoClass没有实现的接口所以它的入口值是0x0000即常量池的0位置这也是前文提到过的为什么常量池从1开始计数的原因。0在不引用常量值的时候使用。
字段表集合
前文已经介绍了Class文件里的常量池、访问标志和继承关系类索引、父类索引、接口索引集合那么在一个Java类的还剩下什么信息没有介绍呢对这一部分我们介绍类的字段。
字段field包括类变量和实例变量但不包括方法内部声明的局部变量。字段数据项的类型是字段表field_info它用于描述接口或类中声明的变量。
字段表field_info结构
想象一下在Java里描述一个字段需要包含哪些信息
字段的作用域public、private、protected实例变量还是类变量static可变性final并发可见性volatile可否被序列化transient字段数据类型基本类型、对象、数组字段名称
字段表结构如图所示 字段表的access_flags
字段表里的access_flags与类中的access_flags非常相似它们都是u2类型且都是访问标志。
除了字段数据类型和字段名称其他的信息都是修饰符都可以用布尔值来表示是否有某一个修饰符。字段访问标志位如图所示 显然ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED只能三选一ACC_FINAL、ACC_VOLATILE只能二选一接口中的字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志。
以GuoClass为例紧随上文中接口索引入口0x0000的是0x0001和0x0002。0x0001是字段表前面的fields_count数据项fields_count用来对字段的数量计数因为GuoClass只有一个int类型的字段money所以fields_count作为一个u2类型的数据项保存了两个字节用十六进制表示数字1就是0x0001。紧随fields_count之后的就是字段表的access_flags数据项它的值是0x0002因为money字段是用private修饰的所以对应的就是ACC_PRIVATE标志。 字段表的name_index和descriptor_index
紧接着access_flags标志的是两个索引值name_index和descriptor_index。
name_index代表字段的简单名称如果是在方法表里代表的是方法的简单名称。例如make()方法的简单名称是“make”money字段的简单名称是“money”。
descriptor_index代表字段或方法的描述符。描述符是用来描述字段的数据类型、方法的参数列表和返回值。描述符的标识字符如图所示 当使用描述符描述数组类型时使用一个前置的“[”例如一个整型数组可以被表示为“[I”一个字符串类型的二位数组可以表示为“[[Ljava/lang/String;”。
当使用描述符描述方法时按照先参数列表再返回值的顺序参数列表按照参数顺序放在一对小括号“()”里。例如
方法int make()的描述符为“()I”方法java.lang.String toString()的描述符为“()Ljava/lang/String;”方法int indexOf(char[]source,int sourceOffset,int sourceCount,char[]target,int targetOffset,int targetCount,int fromIndex)的描述符为“([CII[CIII)I”
以GuoClass为例紧随access_flags的是name_index如图十六进制0x0006再来是descriptor_index十六进制表示为0x0007。 我们可以把索引0x0006和0x0007在常量池中查找一下0x0006保存的是一个CONSTANT_Utf8_info类型的字面量“money”即字段的简单名称。 0x0007在常量池里保存为一个CONSTANT_Utf8_info类型的字面量“I”即字段的描述符表示money字段是int类型的。 至此通过access_flags、name_index和descriptor_index查找到的信息我们可以知道GuoClass里的字段信息是“private int money”。
descriptor_index之后还有一个属性表集合用于保存一些额外的信息。例如final static int money 100000; 会存在一项名称为ConstantValue的属性其值指向常量100000。但是本例中的字段money没有额外信息所以属性计算器为0。 方法表集合
方法表的内容基本上可以参照字段表的内容因为它们的结构几乎一模一样都包括了访问标志access_flags、名称索引name_index、描述符索引descriptor_index、属性表集合attributes这几项。 方法表的access_flags
方法访问标志的取值如图所示。由于volatile和transient关键字不能修饰方法所以在方法访问标志里去掉了ACC_VOLATILE和ACC_TRANSIENT。因为synchronized、abstract、native和strictfp关键字可以修饰方法所以增加了ACC_SYNCHRONIZED、ACC_ABSTRACT、ACC_NATIVE和ACC_STRICTFP标志。 方法表里只不会保存有具体的代码信息方法的java代码被编译器编译成字节码指令保存在属性表集合的“Code”属性里。
public class GuoClassT {private int money;public int make() {return money 1000000000;}
}
以GuoClass为例方法表集合的第一个u2类型的数据是计数器容量它的值为0x0002表示这个类文件有两个方法其中一个显然就是代码中的make()方法另外一个比较隐蔽是实例的构造器方法构造器方法是编译器自动添加的方法。构造器方法是public公有的所以访问标志是ACC_PUBLIC对应的十六进制数是0x0001。 方法表的name_index和descriptor_index
紧挨着构造器方法的访问标志位的是u2类型的方法名称索引其值为0x0008在常量池中我们可以查询到方法名称为。 再往后是u2类型的方法描述符索引其值为0x0009在常量池中我们可以查询到方法描述为()V。前面我们已经提到过这个描述符的含义表示方法没有参数并且返回空void。 属性表计数器的值为0x0001表示属性表集合有一个属性。属性名称索引为0x000A在常量池中查询到其值为“Code”说明此属性是方法的字节码描述。 方法重载
在Java语言中要重载一个方法除了方法名要相同外方法参数的个数或类型不能一样。但是在Class文件格式中只要描述符不是完全一致的两个方法也可以共存即如果两个方法具有相同的方法名方法参数的个数和类型也一样但返回值类型不同这两个方法也是可以合法共存于同一个Class文件里的。