一、Java虚拟机概述
1、Java虚拟机跨语言的平台
1、Java 虚拟机不和包括Java在内的任何语言绑定,它只与"Class文件"这种特定的二进制文件格式所关联
。无论使用何种语言进行软件开发,只要将源文件编译为正确的Class文件,那么这种语言就可以在Java虚拟机上执行。可以说,统一而强大的Class文件结构,就是Java虚拟机的基石、桥梁。所有的JVM全部遵守Java虚拟机规范,也就是说所有的JVM环境都是一样的,这样一来字节码文件可以在各种JVM上运行。
2、想要让一个Java程序正确地运行在JVM中,Java源码就必须要编译为符合JVM规范的字节码
前端编译器
的主要任务就是负责将符合Java语法规范的Java代码转换为符合JVM规范的字节码文件。 javac
是一种能够将Java源码编译为字节码的前端编译器
。 javac
编译器在将Java源码编译为一个有效的字节码文件过程中经历了4个步骤:分别是词法解析、语法解析、语义解析、生成字节码
。
3、Oracle的JDK软件包括两部分内容
一部分是将Java源代码编译成Java虚拟机的指令集的编译器(前端编译器不包含在jvm中)。 另一部分是用于实现Java虚拟机的运行时环境。
2、Java的前端编译器
1、Java源代码的编译结果是字节码,需要有一种编译器能够将Java源码编译为字节码。承担这个重要责任的就是配置在path环境变量中的javac编译器
,它是一种能够将Java源码编译为字节码的前端编译器
。
2、HotSpot VM并没有强制要求前端编译器只能使用javac来编译字节码,其实只要编译结果符合JVM规范并能被JVM所识别即可。
3、在Java的前端编译器领域,除了javac之外,还有一种经常用到的前端编译器,那就是内置在Eclipse中的ECJ(Eclipse Compiler for Java)编译器
。和javac的全量式编译不同,ECJ是一种增量式编译器。
在Eclipse中,当开发人员编写完代码后,使用"Ctrl+S"快捷键时,ECJ编译器所采取的编译方案是把未编译部分的源代码逐行进行编译,而非每次都全量编译
。因此ECJ的编译效率会比javac更加迅速和高效,而编译质量和javac相比大致还是一样的
。 ECJ不仅是Eclipse的默认内置前端编译器,在Tomcat中同样也是使用ECJ编译器来编译jsp文件。由于ECJ编译器是采用GPLv2的开源协议进行源代码公开,因此可以登录Eclipse官网下载ECJ编译器的源码进行二次开发。 默认情况下,IDEA使用javac编译器。(可自行设置为AspectJ编译器ajc)
4、前端编译器并不会直接涉及编译优化等,而是将这些具体优化细节移交给HotSpot的JIT编译器负责
。
3、透过字节码指令看代码细节
public class IntegerTest {
public static void main ( String [ ] args) {
Integer x = 5 ;
int y = 5 ;
System . out. println ( x == y) ; // true,拆箱,比较数值
Integer i1 = 10 ;
Integer i2 = 10 ;
System . out. println ( i1 == i2) ; // true
/**
* 通过字节码和源码可以看出,当Integer的范围在[-128 ~ 127]之间会在数组中直接拿取数值
* 超过这个范围会重新new对象
*/
Integer i3 = 128 ;
Integer i4 = 128 ;
System . out. println ( i3 == i4) ; // false
}
}
部分字节码细节展示
public class StringTest {
public static void main ( String [ ] args) {
//通过字节码发现new了StringBuilder,append后再toString返回str
//toString方法重新new了一个String对象
String str = new String ( "hello" ) + new String ( "world" ) ;
String str1 = "helloworld" ;
System . out. println ( str == str1) ; //false
String str2 = new String ( "helloworld" ) ;
System . out. println ( str == str2) ; //false
}
}
部分字节码细节展示
/**
* 成员变量(非静态的)的赋值过程: ①默认初始化 - ②显式初始化/代码块中初始化 - ③构造器中初始化 - ④有了对象之后,可以“对象.属性”或"对象.方法"
* 的方式对成员变量进行赋值。
*/
public class SonTest {
public static void main ( String [ ] args) {
//情况1:
Father f = new Father ( ) ;
//情况2:
// Father f = new Son();
System . out. println ( f. x) ;
}
}
class Father {
int x = 10 ;
public Father ( ) {
this . print ( ) ;
x = 20 ;
}
public void print ( ) {
System . out. println ( "Father.x = " + x) ;
}
}
class Son extends Father {
int x = 30 ;
public Son ( ) {
this . print ( ) ;
x = 40 ;
}
public void print ( ) {
System . out. println ( "Son.x = " + x) ;
}
}
1、成员变量(非静态的)的赋值过程
:
①默认初始化 ②显式初始化/代码块中初始化 ③构造器中初始化 ④有了对象之后,可以“对象.属性”或"对象.方法"的方式对成员变量进行赋值。
2、情况1的字节码说明
3、情况2的字节码说明
二、Class文件概述
1、什么是字节码文件
源代码经过编译器编译之后会生成一个或多个字节码文件,字节码是一种二进制的类文件,内容是JVM指令,而不像C、C++ 经由编译器直接生成机器码。
2、字节码指令(Byte Code)
Java虚拟机的指令由 一个字节长度的、代表着某种特定操作含义的操作码(opcode)以及跟随其后的0个至多个代表此操作所需参数的操作数(operand)所构成。虚拟机中许多指令并不包含操作数,只有一个操作码。
3、解读字节码的几种方式
1、Notepad++ 中安装HEX-Editor插件,或者使用Binary Viewer软件
2、使用javap指令(jdk自带的反解析工具)javap -v xxx.class > xxx.txt
3、使用IDEA插件:jclasslib或者jclasslib bytecode viewer客户端工具(可视化更好)。
4、Class类的本质
1、任何一个Class文件都对应着唯一的一个类或接口的定义信息,但是反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以动态生成,直接送入类加载器中)。
2、Class文件是一组以8个字节为基础单位的二进制流
,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8个字节进行存储。
3、Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表
。
无符号数
:属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。 表
:是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视作是一张表。由于表没有固定长度,所以通常会在其前面加上个数说明。 无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时候称这一系列连续的某一类型的数据为某一类型的“集合”。
4、Class的结构不像XML等描述语言,由于它没有任何分隔符号,所以在其中的数据项,无论是顺序还是数量,甚至于数据存储的字节序(Byte Ordering,Class文件中字节序为Big-Endian)这样的细节,都是被严格限定的,哪个字节代表什么含义,长度是多少, 先后顺序如何,全部都不允许改变。
5、代码举例
public class Demo {
private int num = 1 ;
public int add ( ) {
num = num + 2 ;
return num;
}
}
对应代码的字节码:
三、Class文件结构
1、概述
1、Class 文件的结构并不是一成不变的,随着Java虚拟机的不断发展,总是不可避免地会对Class文件结构做出一些调整,但是其基本结构和框架是非常稳定的。
2、Class文件的总体结构如下:
魔数 Class文件版本 常量池 访问标志 类索引、父类引用、接口索引集合 字段表集合 方法表集合 属性表集合
3、详细说明
2、Class文件的标志(魔数)
1、魔数(Magic Number)
每个Class文件开头的4个字节的无符号整数称为魔数。 它的唯一作用是校验这个文件是否为一个能被虚拟机接受的有效合法的Class文件
。即魔数是Class文件的标识符,魔数值固定为0xCAFEBABE,不会改变
。 使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,扩展名可随意改动。
2、如果一个Class文件不以0xCAFEBABE开头,虚拟机在进行文件校验的时候会直接抛出错误。
3、Class文件的版本号
1、紧接着魔数的4个字节存储的是Class文件的版本号,同样也是4个字节
。
第5个和第6个字节所代表的含义就是编译的副版本号minor_version。 第7个和第8个字节是编译的主版本号major_version。
2、它们共同构成了Class文件的格式版本号。如,某个Class文件的主版本号为M,副版本号为m,那么这个Class文件的格式版本号为M.m。
3、版本号和Java编译器的对应关系如下:
主版本(十进制) 副版本(十进制) 编译器版本 45 3 1.1 46 0 1.2 47 0 1.3 48 0 1.4 49 0 1.5 50 0 1.6 51 0 1.7 52 0 1.8 53 0 1.9 54 0 1.10 55 0 1.11
4、Java的版本号是从45开始,JDK 1.1 之后的每个JDK大版本发布主版本号向上加1。
5、不同版本的Java编译器编译的Class文件对应的版本是不一样的。目前,高版本的Java虚拟机可以执行由低版本编译器生成的Class文件。但是低版本的Java虚拟机不能执行由高版本编译器生成的Class文件。否则JVM会抛出java.lang.UnsupportedClassVersionError异常
。
6、在实际应用中,由于开发环境和生产环境不同,可能会导致该问题的发生。因此需要在开发时,特别注意开发编译的JDK版本和生产环境中的JDK版本是否一致。虚拟机JDK版本为1.k(k >= 2)时,对应的Class文件格式版本号的范围为45.0 - 44 + k.0(含两端)。
四、Class文件结构之常量池
1、常量池概述
1、紧接着主、次版本号之后的是常量池入口,常量池可以比喻为Class文件里的资源仓库,它是Class文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大
的数据项目之一,另外,它还是在Class文件中第一个出现的表类型数据项目。
2、常量池对于Class文件的字段和方法解析也有着重要的作用
,是整个Class文件的基石。
3、Class文件使用了一个前置的容量计数器(constant_pool_count)加若干个连续的数据项(constant_pool)的形式来描述常量池内容。把这一系列连续常量池数据称为常量池集合
。
2、常量池计数器
1、由于常量池中常量的数量不是固定的,时长时短
,所以在常量池的入口需要放置一项u2类型的无符号数(占用两个字节),代表常量池容量计数值(constant_pool_count)
。
2、与Java中语言习惯不同,这个容量计数是从1而不是0开始的
。表示常量池中有多少项常量
。即constant_pool_count=1
表示常量池中有0个常量项。
3、示例代码中对应的值如下图所示:其值为十六进制数0x0016,即十进制的22,这就代表常量池中有21项常量,索引值范围为[1~21]。
4、对于只有21个常量的解释:在Class文件格式规范制定之时,设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示
。Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都是从0开始
。
3、常量池表概述
1、常量池表(constant_pool[])是一种表结构,以[1,constant_pool_count - 1]为索引
,表明后面有多少个常量项。
2、常量池中主要用于存放编译时期生成的各种字面量(Literal)和符号引用(Symbolic References)
。这部分内容将在类加载后进入方法区的运行时常量池中存放的
。
3、它包含了Class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。常量池中的每一项都具备相同的特征。表结构起始的第一位是个u1(占一个字节)类型的标志位(称为标记字节tag byte),用于确定当前常量属于哪种类型的常量
。
4、字面量和符号引用
1、字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值
等。
2、符号引用则属于编译原理方面的概念,主要包括下面几类常量:
被模块导出或者开放的包(Package) 类和接口的全限定名(Fully Qualified Name) 字段的名称和描述符(Descriptor) 方法的名称和描述符 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic) 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)
3、全限定名:com/itan/middle/day1/Demo就是类的全限定名,仅仅是把包名的“ . ”替换成了“ / ”,为了使连续的多个全限定名之间不混淆产生,使用时最后一般会加入一个“ ; ”表示全限定名结束。
4、简单名称:指没有类型和参数修饰的方法或字段名称,上面示例代码的类的add()方法和num字段的简单名称分别是add和num。
5、描述符:用来描述字段的数据类型、方法的参数列表(包括数量、类型及顺序)和返回值
。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的 void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示。如下图:
6、用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号()之内
。
方法java.lang.String.toString()的描述符为()Ljava/lang/String; 方法int abc(int[] x, int y)的描述符为([II)I。
7、补充说明:
虚拟机在加载Class文件时才会进行动态链接,也就是说,Class文件中不会保存各个方法和字段的最终内存布局信息。因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用。 当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中
。 符号引用:
以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到了内存中。 直接引用:
可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用时与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。
5、常量类型和结构
1、常量池中每一项常量都是一个表,最初常量表中共有11种结构各不相同的表结构数据,后来为了更好地支持动态语言调用,额外增加了4种动态语言相关的常量
在JDK7时增加了前三种:CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和 CONSTANT_InvokeDynamic_info。 在JDK11时又增加了第四种常量CONSTANT_Dynamic_info。 为了支持Java模块化系统 (Jigsaw),又加入了CONSTANT_Module_info和CONSTANT_Package_info两个常量,所以截至JDK 13,常量表中分别有17种不同类型的常量。
2、细节说明
CONSTANT_Class_info结构用于表示类或接口 CONSTANT_Fieldref_info、CONSTANT_Methodref_info和CONSTANT_InterfaceMehodref_info结构表示字段、方法和接口方法 CONSTANT_String_info结构用于表示String类型的常量对象 CONSTANT_Integer_info和CONSTANT_Float_info表示4字节(int和float)的数值常量 CONSTANT_Long_info和CONSTANT_Double_info结构表示8字节(long和double)的数值常量 在Class文件的常量池表中,所有的8字节常量均占两个表成员(项)的空间。如果一个 CONSTANT_Long_info或CONSTANT_Double_info结构的项在常量池表中的索引位n,则常量池表中下一个可用项的索引位为n+2,此时常量池表中索引为n+1的项仍然有效但必须视为不可用的。 CONSTANT_NameAndType_info结构用于表示字段或方法,但是和之前的3个结构不同,它没有指明该字段或方法所属的类或接口。 CONSTANT_Utf8_info 用于表示字符常量的值 CONSTANT_MethodHandle_info结构用于表示方法句柄 CONSTANT_MethodType_info结构表示方法类型 CONSTANT_InvokeDynamic_info结构用于表示invokedynamic指令所用到的引导方法(bootstrap method)、引导方法所用到的动态调用名称(dynamic invocation name)、参数和返回类型,并可以给引导方法传入一系列称为静态参数(static argument)的常量。
6、解析字节码得到常量池中所有常量
1、将示例代码的二进制字节码导入到excel中方便读取,0a对应的十进制数是10,再去常量类型表中找标志为10的常量(即CONSTANT_Methodref_info)。此常量占用5个字节。
2、需要注意的是当tag=1时为字符串,长度是不确定的,比如该字节码的第二行中,在常量类型表中发现tag后面是类型为u2的length,表示字符占用3个字节,也就是字符串本身。
3、所有常量标识完成之后,如下图
4、对应常量类型和使用jclasslib工具查看字节码的类型一致
7、解读字节码
1、以第一项为例,首先第一项的tag为0a,对应十进制为10即tag=10的类型常量CONSTANT_Methodref_info,紧跟tag之后的是一个u2类型的索引项(04),作用是指向声明方法的类描述符的索引
,也就是指向了tag=7的类型常量CONSTANT_Class_info。
2、CONSTANT_Class_info的tag后面是一个u2类型的索引项(15),作用是指向全限定名常量项的索引
,15对应十进制为21,也就是索引为21的常量项即tag=1的类型常量CONSTANT_Utf8_info。
3、CONSTANT_Utf8_info是描述一个字符串的信息,它的tag后面是一个u2类型的length(10),表示字符串占用的长度(10对应十进制为16),也就是从6a到74这段内容。
4、从字节码工具上可以看到6a到74对应的ASCII值为java/lang/object
,相当于告诉我们当前这个方法的类型是java.lang.Object类定义的。
5、在04之后又是一个u2类型的索引项(12),作用是指向方法名称和类型描述符(返回值类型,形参等)
,12对应的十进制为18,也就是索引为18的常量项即tag=12的类型常量CONSTANT_NameAndType_info
6、CONSTANT_NameAndType_info的tag后面是两个u2类型的索引项,分别指向方法名称和方法描述符
,也就是索引为7和8的常量项即tag=1的类型常量CONSTANT_Utf8_info。
7、分别对应3c到3e、28到56这段的内容,从字节码工具上可以看到3c到3e这段对应的ASCII值为<init>
,28到56这段对应ASCII值为()V
,的相当于告诉我们当前这个方法的名称为init,形参为空,返回值类型为V。
8、总结
1、表开始的第一位是u1类型的标志位(tag),代表当前这个常量项使用的是哪种表结构,哪种常量类型。
2、在常量池列表中,CONSTANT_Utf8_info 常量项是一种使用改进过的UTF-8编码格式来存储诸如文字字符串、类或接口的全限定名、字段或方法的简单名称以及描述符等常量字符串信息。
3、这17中常量项结构还有一个特点是,其中16个常量项占用的字节固定,只有CONSTANT_Utf8_info 占用字节不固定,其大小由length决定。因为从常量池存放的内容可知,其存放的是字面量和符号引用,最终这些内容都会是一个字符串,这些字符串的大小是在编写程序时才确定
。比如定义一个类,类名可以取长取短,在没编译前,大小不固定,编译后,通过utf-8编码,就可以知道其长度。
4、常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型(后面的很多数据类型都会指向此处),也是占用Class文件空间最大的数据项目之一。
5、常量池中为什么要包含这些内容?
Java代码在进行javac编译时,并不像C/C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态链接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址中。
五、Class文件结构之访问标志
1、访问标志概述
1、在常量池结束之后,紧跟着访问标记(access_flags)。该标记使用两个字节表示。用于识别一些类或者接口层次的访问信息,包括:
这个Class是类还是接口。 是否定义为public类型。 是否定义为abstract类型。 如果是类的话,是否被声明为final。
2、各种访问标志如下
3、类的访问权限通常为ACC_开头的常量,每一种类型的表示都是通过设置访问标记的32位中的特定位来实现的。若是public final的类,则该标记位ACC_PUBLIC|ACC_FINAL
。
4、使用ACC_SUPER可以让类更准确地定位到父类的方法super.method(),现在编译器都会设置并使用这个标记。
5、示例代码Demo是一个普通的Java类,不是接口、枚举、注解或者模块,被public关键字修饰但没有被声明为final和abstract,并且它使用了JDK 1.2之后的编译器进行编译,因此它的ACC_PUBLIC、ACC_SUPER标志应当为真,而ACC_FINAL、ACC_INTERFACE、 ACC_ABSTRACT、ACC_SYNTHETIC、ACC_ANNOTATION、ACC_ENUM、ACC_MODULE这七 个标志应当为假,因此它的access_flags的值应为:0x0001|0x0020=0x0021。
2、补充说明
1、带有ACC_INTERFACE标志的Class文件表示的是接口而不是类,反之则表示的是类而不是接口。
如果一个Class文件被设置了ACC_INTERFACE标志,那么同时也得设置ACC_ABSTRACT标志。并且不能再设置ACC_FINAL、ACC_SUPER或ACC_ENUM标志
。 如果没有设置ACC_INTERFACE标志,那么这个Class文件可以具有上表中除ACC_ANNOTATION外的其他所有标志。当然,ACC_FINAL和ACC_ABSTRACT这类互斥的标志除外。这两个标志不能同时设置
。
2、ACC_SUPER标志用于确定类或接口里面的invokespecial指令使用的是哪一种执行语义。针对Java虚拟机指令集的编译器都应当设置这个标志
。
对于Java8及以后版本来说,无论Class文件中这个标志的实际值是什么,也不管Class文件的版本号是多少,Java虚拟机都认为每个Class文件均设置了ACC_SUPER标志。 ACC_SUPER标志是为了向后兼容由旧Java编译器所编译的代码而设计的。目前的ACC_SUPER标志在由JDK1.0.2之前的编译器所生成的access_flags中是没有确定含义的,如果设置了该标志,那么Oracle的Java虚拟机实现会将其忽略。
3、ACC_SYNTHETIC标志意味着该类或接口是由编译器生成的,而不是由源代码生成的。
4、注解类型必须设置ACC_ANNOTATION标志。如果设置了ACC_ANNOTATION标志,那么也必须设置ACC_INTERFACE标志。
5、ACC_ENUM标志表明该类或其父类为枚举类型。
6、表中没有使用的access_flags标志是为未来扩充而预留的,这些预留的标志在编译器中应该设置为0,Java虚拟机实现也应该忽略它们。
六、Class文件结构之类索引、父类索引与接口索引集合
1、概述
1、类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定该类的继承关系
。
2、类索引(this_class)
1、类索引是一个2字节无符号整数,指向常量池的索引
。它提供了类的全限定类名,如com/itan/middle/day1/Demo。this_class的值必须是对常量池表中某项的一个有效索引值
。常量池在这个索引处的成员必须为CONSTANT_Class_info类型结构体,该结构体表示这个Class文件所定义的类或接口。
2、作用:用于确定这个类的全限定名
。
3、父类索引(super_class)
1、父类索引是一个2字节无符号整数,指向常量池的索引
。它提供了当前类的父类的全限定名。如果没有继承任何类,其默认继承的是java/lang/Object类
。
2、作用:用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0
。
3、注意:super_class指向的父类不能是final
4、接口索引集合(interfaces)
1、接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements关键字(如果该类本身是一个接口,则是extends语句)后的接口顺序从左到右排列在接口索引集合中。
2、指向常量池索引集合,它提供了一个符号引用到所有已实现的接口。
3、由于一个类可以实现多个接口,因此需要以数组形式保存多个接口的索引,表示接口的每个索引也是一个指向常量池的CONSTANT_Class(必须是接口)。
4、接口计数器(interfaces_count):它的值表示当前类或接口的直接超接口数量。
5、接口索引集合(interfaces [ ]):
interfaces [ ]中每个成员的值必须是对常量池表中某项的有效索引值,它的长度为interfaces_count。 每个成员interfaces [i]必须为CONSTANT_Class_info结构,其中i的值为为[0,interfaces_count)。 在interfaces [ ]中,各成员所表示的接口顺序和对应的源代码中给定的接口顺序(左至右)一致,即interfaces[0]对应的是源代码中最左边的接口(也就是紧靠implements关键字的第一个接口)。
七、Class文件结构之字段表集合
1、概述
1、用于描述接口或类中声明的变量。字段(field)包括类级变量及实例级变量,但不包括方法内部、代码块内部声明的局部变量(local variables)
。
2、字段的名字、字段被定义的数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。
3、它指向常量池索引集合
,它描述了每个字段的完整信息。比如字段的标识符、访问修饰符(public、private或protected)、是类变量还是实例变量(static修饰符)、是否是常量(final修饰符)等
。
4、补充说明:
字段表集合中不会列出从父类或实现的接口中继承而来的字段,但有可能列出原本Java代码中不存在的字段。比如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。 在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但对于字节码来讲,如果两个字段的描述符不一样,那字段重名就是合法的。
2、字段计数器
1、字段计数器(fields_count):它的值表示当前Class文件fields表的成员个数,使用两个字节来表示。
3、字段表
1、fields表中的每个成员都是一个field_info结构,用于表示该类或接口所声明的所有类字段或者实例字段,不包括方法内部声明的变量,也不包括从父类或父接口继承的字段
。
2、一个字段所包含的信息如下,这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫做什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。
作用域(public、private、protected修饰符) 是实例变量还是类变量(static修饰符) 可变性(final修饰符) 并发可见性(volatile修饰符,是否强制从主内存读写) 可否被序列化(transient修饰符) 字段数据类型(基本类型、对象、数组) 字段名称
4、字段表结构
1、字段表作为一个表,同样拥有自己的结构,如下
2、字段表访问标志:一个字段可以被各种关键字去修饰,比如:作用域修饰符(public、private、protected)、static 修饰符、final 修饰符、volatile 修饰符等等。因此,其可像类的访问标志那样,使用一些标志来标记字段。
注意:由于语法规则的约束,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志最多只能选择其一,ACC_FINAL、ACC_VOLATILE不能同时选择。接口之中的字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志,这些都是由Java本身的语言规则所导致的
。
3、字段名索引:根据字段名索引的值,查询常量池中的指定索引项即可知道字段的简单名称
。
4、描述符索引:
描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值
。 根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)及代表无返回值的void类型都用一个大写字符来表示,而对象则用字符L加对象的全限定名来表示。如下所示
5、属性表集合:一个字段还可能拥有一些属性,用于存储更多的额外信息。比如初始化值、一些注释信息等
。属性个数存放在attribute_count中,属性具体内容存放在attributes数组中。以常量属性为例,它的结构如下图:
5、字段表数据解读
1、在字段计数器之后的是一个u2类型的字段访问标志。对应的十六进制值为0x0002,代表private
修饰符的ACC_PRIVATE标志位为真(通过查表发现ACC_PRIVATE标志的值为0x0002),其他修饰符为假。
2、紧接访问标志之后的是一个u2类型的字段名索引,对应的十六进制值为0x0005,指向常量池表中索引为5的常量(CONSTANT_Utf8_info类型的字符串)。对应的值为num
。
3、紧跟字段名索引之后的是一个u2类型的描述符索引,其值为0x0006,指向常量池表中索引为6的常量(CONSTANT_Utf8_info类型的字符串)。对应的值为I
。根据上面的信息可以推断出原代码定义的字段为private int num;
4、紧跟描述符索引之后的是一个u2类型的字段属性计数器,其值为0x0000,表示该字段没有额外的描述信息,因此属性集合也应该为空。