01-彻底搞懂JVM之类文件结构

概述

class文件全名称为Java class文件,主要在平台无关性和网络移动性方面使Java更适合网络。它在平台无关性方面的任务是:为Java程序提供独立于底层主机平台的二进制形式的服务。class文件打破了C或者C++等语言所遵循的传统,使用这些传统语言写的程序通常首先被编译,然后被连接成单独的、专门支持特定硬件平台和操作系统的二进制文件。通常情况下,一个平台上的二进制可执行文件不能在其他平台上工作。而Java class文件是可以运行在任何支持Java虚拟机的硬件平台和操作系统上的二进制文件。

前言

在了解类加载器之前,我想先介绍一下class文件的文件结构,大家都知道,每一种文件的拓展名都是不一样的,所以程序语言也不例外java的叫.class,python的叫.py等,不得不说的是,这个class的文件的确是一个重大的事件,在虚拟机的世界里.class文件也占据着一个很重要的位置,它是java实现一次编译,到处运行的基石,在虚拟机与高级程序语言中间,它充当着一个适配器的作用.同时它在java版本兼容这件事上起到了不可估量的作用,想要学习好虚拟机,了解class的文件结构是无法避免的一件事.
我们java程序员见到最多的估计就是.java文件了,大家都知道java程序代码经过编译以后会成为.class的二进制文件,但是class到底长什么样子,大家又知道多少,如果连它是什么都不清楚,又谈何用好它呢,这篇文章我们就好好的研究一次,class到底是个啥?

Class类文件结构

在开始介绍之前,大家可以搞一个大佬们写的工具来查看一下自己的写代码编译后的class文件,我是用classpy搞得,先上一个我写的java代码
在这里插入图片描述
很简单的一段代码,我们用classpy查看一下编译后的class到底长啥样,如下:
在这里插入图片描述
当然你也可以用javap -verbose去查看,但是那个查看的维度不同而已。

数据项

  1. class一般都说是二进制流文件,为啥说是二进制流呢,其实大家都遇到过,class文件并不是只以磁盘文件的形式存在,在类加载器和动态编译以及远程调用等等过程中,都是以流的方式直接被虚拟机加载进内存的,所以说任意一个Class类文件都对应着唯一的一个类或者接口的定义信息,但是不代表一个类就一定对应这一个Class文件,因为它可能还是个流.
  2. Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数 据,没有空隙存在。当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前(这种顺序称为“Big-Endian”,具体顺序是指按高位字节在地址最低位,最低字节在地址最高位来存储数据,它是SPARC、PowerPC等处理器的默认多字节存储顺序,而x86等处理器则是使用了相反的“Little-Endian”顺序来存储数据,也就是大端模式)的方式分割成若干个8个字节进行存储。
  3. 根据《Java虚拟机规范》的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数

无符号数 属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串 值。

表 是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名 都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视 作是一张表,这张表由表6-1所示的数据项按严格顺序排列构成。

  1. 无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的 容量计数器加若干个连续的数据项的形式,这时候称这一系列连续的某一类型的数据为某一类型的“集 合”。Class的结构不像XML等描述语言,由于它没有任何分隔符号,所以无论是顺序还是数量,甚至于数据存储的字节序(Byte Ordering,Class 文件中字节序为Big-Endian)这样的细节,都是被严格限定的,哪个字节代表什么含义,长度是多少, 先后顺序如何,全部都不允许改变。
  2. 就像数组下标一样,在写算法时,每个下标中的数值都代表了某一个对象或业务,如果随意打破这种规则,那这个数组的构成将变得没有意义了,下边这张表就是Class类文件严格按照顺序定义个文件格式数据项,与我们通过classpy查看的class文件结构对应
类型名称含义数据项数量
u4magic魔数1
u2minjor_version次版本号1
u2major_version主版本号1
u2constant_pool_count常量的个数1
cp_infoconstant_ pool常量池表constant_pool_count-1
u2access_ flags类的访问控制权限1
u2this_class类名1
u2super_class父类名1
u2interfaces_count接口数量1
u2interfaces接口名interfaces_count
u2fields_count字段个数1
field_infofields字段表fields_count
u2methods_count方法个数1
method_infomethods方法表methods_count
u2attributes_count附加属性个数1
attribute_infoattributes附加属性表attributes_count

到这里估计也就明白啥叫 u1,u2,u4,u8和_info了吧,从上到下要严格按照class定义的规范去设定数据项,所以说从magic到attributes的顺序必须是这样,然后虚拟机才会在类加载的 验证 过程中去效验通过,下边咱就一个一个介绍这些数据项代表的含义

魔数

  • 在计算机领域,魔数有两个含义,一指用来判断文件类型的魔数;二指程序代码中的魔数,也称魔法值。所谓魔数就是指在代码中出现但没有解释的数字常量或字符串,又称魔法值。如果在某个程序中你使用了魔数,那么在几个月或几年后你将很可能不知道它的含义是什么。大多数情况下,我们都是通过扩展名来识别一个文件的类型的,比如我们看到一个.txt类型的文件我们就知道他是一个纯文本文件。但是,扩展名是可以修改的,当一个文件的扩展名被修改过,怎么识别一个文件的类型呢?这就用到了我们提到的“魔数”。很多类型的文件,其起始的几个字节的内容是固定的(或是有意填充,或是本就如此)这几个字节的内容也被称为魔数,因为根据这几个字节的内容就可以确定文件类型。有了这些魔术数字,我们就可以很方便的区别不同的文件。
  • 举个很简单的例子,比如说我们想把一个png的文件转换成pdf,小白就有这么干的,把.png的拓展名改成.pdf,然后用阅读器去打开这个pdf,然后发现打不开,会报类似不支持该类型或文件被损坏的错误,其实这个例子就是魔数存在的意义,再比如说,大家想学习技术,在网上找了很多的早期加密视频,通过更改与或的方式去解密视频来进行观看,但是大家在用WinHex去分析文件的时候,往往都会发现,这些文件的
  • 常见的文件魔数我举几个例子,大家了解一下就好,有兴趣的可以去深入研究
格式魔数文件头文件结尾
classCA FE BA BE
JPGFF D8 FF E0FFD9
GIF47 49 46 38
PSD38 42 50 53
PNG89 50 4E 47AE 42 60 82
GIF47 49 46 3800 3B
ZIP50 4B 03 0450 4B
xls/docD0 CF 11 E0
PDF25 50 44 46 2D 31 2E
RAR52 61 72 21

每个Class文件的前4个字节,都是class文件的魔数(magic) 都是十六进制的 x0CAFEBABE,这个魔数很有意思,咖啡宝贝,估计也是java为啥用咖啡做logo的原因吧,这个魔数的唯一作用就是让虚拟机识别,虚拟机也只认识这个魔数开头的文件,就像上边说的,就算你拓展名怎么改,是png文件就是png,换个马甲阅读器一样不鸟你,class也是一样.不管是java语言还是JRuby还是Groovy,只要编译后的文件符合class文件的规范,就可以被虚拟机加载并使用.

次版本号和主版本号

紧挨着魔数4个字节之后的第5和第6个字节,也就是次版本号了,第7和第8个字节就是主版本号,java定义的版本号是从45开始的,从jdk1.1之后,规定的就是每次发布一个大版本,主版本号就加1,从jdk1.0到1.1使用了45到45.3的版本号,这也是jdk向下兼容的主要基础,高版本的可以执行低版本的class,但是低版本不能运行高版本的class文件,因为《Java虚拟机规范》在Class文 件校验部分明确要求了即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件

JDK版本号Class主版本号可执行的版本号范围
1.145.045
1.246.045~46
1.347.045~47
1.448.045~48
1.549.045~49
1.650.045~50
1.751.045~51
1.852.045~52
1.953.0045~53
1.1054.045~54
1.1155.045~55
1.1256.045~56
1.1357.045~57
1.1458.045~58
1.1559.045~59
1.1660.045~60

关于次版本号,曾经在现代Java(即Java 2)出现前被短暂使用过,JDK 1.0.2支持的版本45.0~ 45.3(包括45.0~45.3)。JDK 1.1支持版本45.0~45.65535,从JDK 1.2以后,直到JDK 12之前次版本 号均未使用,全部固定为零。而到了JDK 12时期,由于JDK提供的功能集已经非常庞大,有一些复杂 的新特性需要以“公测”的形式放出,所以设计者重新启用了副版本号,将它用于标识“技术预览版”功 能特性的支持。如果Class文件中使用了该版本JDK尚未列入正式特性清单中的预览功能,则必须把次 版本号标识为65535,以便Java虚拟机在加载类文件时能够区分出来。
我上边例子用的是jdk1.8,然后我们可以看到在第7位的数字就是52,第8位是0
在这里插入图片描述

常量池表

关于class文件结构中的表类数据项,也就是以_info结尾的数据项,因为他们的容量都是根据代码中数据的多少来决定的,所以在常量池中的常量数目是无法确定的,所以在常量池的入口处,定义了一个2个字节的数据项,与常量池表对应的也就是常量池个数(constant_pool_count 可以理解为常量池容量计数器),这个个数说的是常量池的个数,并不是常量池中常量的多少,常量池的容量是通过这个数据项记录的,也就是key唯一,value可以写1,2,3,4,5…,我们来看第9和第10位记录的数据如下:
在这里插入图片描述
这里记录了常量池中的数据个数是67,正好与上边我们展示的类结构中的数值对应.没学习编程之前都认为数都是从1开始数的,学了编程以后才知道,哦! 数得从0开始数,但是这个常量池个数的计数器就是个例外,和java中经常以0开头的定义有点区别,它是从1开始计数的,为啥不用0呢,也是有含义在里边的,因为在虚拟机中有时候需要需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示。并且Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口表、字段表、方法表等等的容量计数都与一般习惯相同,是从0开始.因为我们有67个数据,也就对应着0-66的索引,0保留它用的话,那索引值也就是从1到66,咱们看一下下图,类文件中就是这样表示的.
在这里插入图片描述

在这里插入图片描述
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
在计算机科学中,字面量是用于表达源代码中一个固定值的表示法(notation)几乎所有计算机编程语言都具有对基本值的字面量表示,诸如:整数、浮点数以及字符串;而有很多也对布尔类型和字符类型的值也支持字面量表示;还有一些甚至对枚举类型的元素以及像数组、记录和对象等复合类型的值也支持字面量表示法。字符串字面量(StringLiteral)是指双引号引住的一系列字符,双引号中可以没有字符,可以只有一个字符,也可以有很多个字符。这里的字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念。
在这个常量池表中存储的内容大致有以下几种:

  • 被模块导出或者开放的包(Package)
  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符
  • 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
  • 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)

Java代码在进行Javac编译的时候,是在虚拟机加载Class文件的时候通过动态连接进行的,也就是说,在Class文件中并不会保存各个方法、字段最终在内存中的布局信息,这些字段、方法的符号引用不经过虚拟机在运行期间去转换的话是无法得到真正的内存入口地址的,也就无法直接被虚拟机使用的。当虚拟机做类加载时,将会从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。关于类的创建和动态连接的内容, 我们后续博文中再详细介绍,你可以简单理解为map中的key,Class文件中的常量池记录的都是一堆key,而你在逻辑中想用真正的数值的时候,必须通过这个key去找到对应的value才可以操作value进行业务逻辑操作,这个value就是直接引用,也就是内存中真实的指针入口地址。

在这里插入图片描述
关于常量池内部的信息,也就是咱们例子中1-66的索引位中的数据都代表了什么,其实可以像理解上边内容一样去理解常量池中的数据,常量池中的每一个常量也有自己的固定格式和内容,比如上图中的1-3的索引位,我们可以看到每一个索引位对应的数据都有一个tag标签,专业名词怎么定义这块内容我也不太清楚,咱就以“常量表”来定义每一个索引位置的结构,简单点说就是常量池表中有n个常量表,每个常量表都不一样,但是有一个共性,那就是第一位都是tag,它是一个u1类型的,也就是占用1个字节,用来表示这个常量是那种常量类型,总结一句话就是常量池表中的每个常量表都会用起始位的第一个字节来描述当前这个常量表内容属于那种常量类型,常量类型目前有以下几种,数值对应着tag的数值,也就是所属的常量类型

常量类型tag数值数值含义
CONSTANT_Utf8_info1UTF-8编码的字符串
CONSTANT_Integer_info3整型字面量
CONSTANT_Float_info4浮点型字面量
CONSTANT_Long_info5长整型字面量
CONSTANT_Double_info6双精度浮点型字面量
CONSTANT_Class_info7类或接口的符号引用
CONSTANT_String_info8字符串类型字面量
CONSTANT_Fieldref_info9字段的符号引用
CONSTANT_Methodref_info10类中方法的符号引用
CONSTANT_InterfaceMethodref_info11接口中方法的符号引用
CONSTANT_NameAndType_info12字段或方法的部分符号引用
CONSTANT_MethodHandle_info15方法句柄
CONSTANT_MethodType_info16方法类型
CONSTANT_Dynamic_info17动态计算常量
CONSTANT_InvokeDynamic_info18动态方法调用点
CONSTANT_Module_info19模块
CONSTANT_Package_info20模块中开放或者导出的包

在常量表中定义的常量类型有17种,每一种类型的结构信息也不一样,常量表中的内容有兴趣的可以继续研究,全部展开讲内容就太多了,可以来看一个javap和classpy的映射关系
在这里插入图片描述
这里有一个点比较有意思,由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以CONSTANT_Utf8_info型常量的最大长度也就是Java中方法、字段名的最大长度。
在这里插入图片描述
而tag是1的常量类型中有三项,tag,length和bytes,length是个u2类型的,tag和bytes都是u1,所以length能表达的最大值也就是2个字节8个bit位,换算成十进制最大也就是65535这个数。所以Java程序中如果定义了超过(65535/1024)≈ 64KB 英文字符的变量或方法名,即使规则和全部字符都是合法的,也会无法编译。下边是我的实验结果
在这里插入图片描述
同时这个length也表示的bytes内容的长度,也表示着在引用的内容位数长度。也就是我们用javap翻译出来的//后的内容。

类的访问控制权限

在常量池结束之后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或 者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract 类型;如果是类的话,是否被声明为final等等。
它的访问标识见下表:

标识含义
ACC_PUBLIC0x0001是否是public类型
ACC_FINAL0x0010是否被声明为了final,只有类可以设置
ACC_SUPER0x0020是否允许使用invokespecial字节码指令的新语义,invokespecial指令的语义在jdk1.0.2发生过改变,为了区别这条指令使用哪种语义,jdk1.0.2之后编译出来的类这个标志必须都是真
ACC_INTERFACE0x0200是否是接口
ACC_ABSTRACT0x0400是否是abstract类型,当是接口或抽象类时为真,其他都是假
ACC_SYNTHETIC0x1000这个类不是由用户代码生成
ACC_ANNOTATION0x2000是否是注解
ACC_ENUM0x4000是否是枚举
ACC_MODULE0x8000是否是模块

在这里插入图片描述
access_flags中一共有16个标志位可以使用,当前只定义了其中9个,没有使用到的标志位要求一 律为零。我们这个例子就是一个普通得Java类,不是接口、枚举、注解也不是模块,也没有声明为final和abstract,只是用public关键字修饰了,用编译器编译之后,它的ACC_PUBLIC、ACC_SUPER标志应当为真,而ACC_FINAL、ACC_INTERFACE、 ACC_ABSTRACT、ACC_SYNTHETIC、ACC_ANNOTATION、ACC_ENUM、ACC_MODULE这七个标志应当为假,因此它的access_flags的值应为:0x0001|0x0020=0x0021。从上边的图里也能看到,access_flags 标志的确为0x 0021。

类名和父类名和接口

类(this_class)和父类(super_class)这两项都是一个u2类型的数据,而接口 (interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定该类型的继承关系。类用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了 java.lang.Object外,所有Java类的父类索引都不为0。类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类 型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过 CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的 全限定名字符串。
这句话啥意思呢,我用图一说你就明白了,比如我们先看到的是这个:
在这里插入图片描述
先忽略掉马赛克中的内容,咱再去看看怎么找到马赛克的内容,首先我们要找到this_class和super_class这两组u2类型的字节码内容,如下:
在这里插入图片描述
我们可以找到thisClass对应的是0011,superClass对应的是0012,换算以后也就是#17和#18的两个符号引用,然后再回顾常量池表中的常量索引,自然就可以找到17和18的索引项内容,如下:
在这里插入图片描述
然后发现#17和#18分别对应的是 070037 和070038,当然07指的是tag,0037和0038指的才是内容,这个工具其实已经给我们换算好了,也就是55和56,然后咱们再去找#55和#56,如下:
在这里插入图片描述
在这里,我们才算真正找到了这两个符号引用对应的全限定名字符串常量。
这里我有个猜测,但是也不确定到底对不对,留给大家一起验证好了,大家都知道,java是单继承的,所有的继承都是通过extends显式声明的,但是又流传着一个说法,那就是Object对象是所有java类(不含Object)的父对象,可是我们并没有用extends关键字,它为啥就能继承了呢?网上说法也有很多,什么jdk6之前是编译期处理的,jdk1.7之后是虚拟机处理的,但是到底怎么处理的?为啥这么处理?始终找不到答案,我用jad反编译后的也是没有extends的,对于这个问题我也很是困惑,但是我想到了一个点,那就是java虚拟机规范中规定了类的文件结构是有super_class这一项的,也就是说在顺序固定的前提下,super_class必须有,而且它也不像常量池和接口那样前边有个单独的字节去标识容量大小,那是不是也就意味着,不管你this_class再怎么折腾,只要没有extends去显式继承某个类,虚拟机在处理时,都要严格按照虚拟机规范去填充一个默认值,所以这里才有了这个Object是所有java对象父类的概念呢,当然也不一定哈,这只是我自己的一个猜测而已。

接口索引计数器(interfaces_count)和接口索引集合(interfaces)就用来描述这个类实现了哪些接口,这些被实现的接口将按implements关键字(如果这个Class文件表示的是一个接口,则应当是 extends关键字)后的接口顺序从左到右排列在接口索引集合中。接口索引计数器是个u2类型的,是它下边接口索引集合的容量值,也就是集合中元素的个数,如果这个类没有实现任何接口,那就是0。接口索引集合是一组u2类型的数据集合。集合中的每一项都是按照顺序实现的接口信息,和找类的过程是一样的。

字段表

字段表(field_info)用于描述接口或者类中声明的变量。Java语言中的“字段”(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。读者可以回忆一下在Java语言中描述一个字段可以包含哪些信息。字段可以包括的修饰符有字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、 字段名称。上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫做什么名字、字段被定义成什么数据类型,这些都是无法固定的,只能引用常 量池中的常量来描述。
字段表也有自己的数据项格式,如下表:
在这里插入图片描述

类型名称数量
u2access_flags1
u2name_index1
u2descriptor_index1
u2attributes_count1
attributes_infoattributesattributes_count

字段修饰符放在access_flags项目中,它与类中的access_flags项目有些类似的,都是一个u2的数据类型。attributes_count和attributes类似于fields_count和fields,内容的查找过程和上边类中介绍的一样。字段的修饰符有下表中的这些标识符

标识含义
ACC_PUBLIC0x0001是否是public
ACC_PRIVATE0x0002是否是private
ACC_PROTECTED0x0004是否是protected
ACC_STATIC0x0008是否是static
ACC_FINAL0x0010是否是final
ACC_VOLATILE0x0040是否是volatile
ACC_TRANSIENT0x0080是否是transient
ACC_SYNITHETIC0x1000是否由编译器自动产生
ACC_ENUM0x4000是否是枚举

很明显,由于语法规则的约束,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志最多只能选择其一,ACC_FINAL、ACC_VOLATILE不能同时选择。接口之中的字段必须有 ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志,这些都是由Java本身的语言规则所导致的。
跟随access_flags标志的是两项索引值:name_index和descriptor_index。它们都是对常量池项的引 用,分别代表着字段的简单名称以及字段和方法的描述符。现在需要解释一下“简单名称”“描述符”以 及前面出现过多次的“全限定名”这三种特殊字符串的概念。 全限定名和简单名称很好理解,类的全限定名,仅仅是把类全名中的“.”替换成了“/”而已,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”号表示全限定名结束。简单名称则就是指没有类型和参数修饰 的方法或者字段名称,比如我们文章开头例子中的getString()方法和a,s等参数,简单名称就是getString和a还有s。 相比于全限定名和简单名称,方法和字段的描述符就要复杂一些。描述符的作用是用来描述字段 的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类 型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大 写字符来表示,而对象类型则用字符L加对象的全限定名来表示。详情见下表

描述符标识含义
B表示byte类型
C表示char类型
D表示double类型
F表示float类型
I表示int类型
J表示long类型
S表示short类型
Z表示boolean类型
V表示void
L表示对象类型
[表示数组,n维数组就是n个[
()表示方法

用描述符来描述方法时,按照先参数列表、后返回值的顺序描述,参数列表按照参数的严格顺序 放在一组小括号“()”之内。比如方法int indexOf(char[]source,int sourceOffset,int sourceCount,char[]target, int targetOffset,int targetCount,int fromIndex)的描述符为“([CII[CIII)I”。
字段表集合中不会列出从父类或者父接口中继承而来的字段,但有可能出现原本Java代码之中不 存在的字段,譬如在内部类中为了保持对外部类的访问性,编译器就会自动添加指向外部类实例的字 段。另外,在Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使 用不一样的名称,但是对于Class文件格式来讲,只要两个字段的描述符不是完全相同,那字段重名就是合法的。
字段表中的attributes和attributes_count指向的不是常量池而是附加属性集合attributes中的索引项,比如例子中被final修饰的AA
在这里插入图片描述

方法表

Class文件存储 格式中对方法的描述与对字段的描述采用了几乎完全一致的方式,方法表的结构如同字段表一样,依 次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表 集合(attributes)几项,这些数据项目的含义也与字段表中的非常类似,仅在访问标 志和属性表集合的可选项中有所区别。

类型名称数量
u2access_flags1
u2name_index1
u2descriptor_index1
u2attributes_count1
attributes_infoattributesattributes_count

因为volatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有了 ACC_VOLATILE标志和ACC_TRANSIENT标志。与之相对,synchronized、native、strictfp和abstract 关键字可以修饰方法,方法表的访问标志中也相应地增加了ACC_SYNCHRONIZED、 ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志。

标识含义
ACC_PUBLIC0x0001是否是public
ACC_PRIVATE0x0002是否是private
ACC_PROTECTED0x0004是否是protected
ACC_STATIC0x0008是否是static
ACC_FINAL0x0010是否是final
ACC_SYNCHRONIZED0x0020是否是synchronized
ACC_NATIVE0x0100是否是native
ACC_STRICTFP0x0800是否是strictfp
ACC_ABSTRACT0x0400是否是abstract
ACC_BRIDGE0x0040是否由编译器产生的桥接方法
ACC_VARARGS0x0080是否能接受不定参数
ACC_SYNTHETIC0x1000是否由编译器自动产生

方法里的Java代码经过编译器编译成字节码指令后,存放在方法表集合中的属性表集合中的元素code里;这个属性表的名称为“Code”。附加的属性表是Class文件格式中最具扩展性的一种数据项目,将在下边讲解。
在这里插入图片描述
与字段表集合相对应的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息,但可能出现编译器自动添加的方法,最典型的便是类构造器<clinit>方法和实例构造器<init>方法。
在Java语言中,重载(Overload)一个方法就需要满足下边两点
1、要与原方法具有相同的简单名称。
2、要与原方法有不同的特征签名。
特征签名是指一个方法中各个参数在常量池中的字段符号引用的集合,也正是因为返回值不会包含在特征签名之中,所以Java语言里面是无法仅仅依靠返回值 的不同来对一个已有方法进行重载的。但是在Class文件格式之中,特征签名的范围明显要更大一些, 只要描述符不是完全一致的两个方法就可以共存。也就是说,如果两个方法有相同的名称和特征签 名,但返回值不同,那么也是可以合法共存于同一个Class文件中的。Java代码的方法特征签名只包括方法名称、参数顺序及参数类型;而字节码的特征签名还包括方法返回值以及受查异常表。

属性表集合

属性表(attribute_info)在前面的讲解之中已经出现过数次,Class文件、字段表、方法表都可以 携带自己的属性表集合,以描述某些场景专有的信息。 与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松一 些,不再要求各个属性表具有严格顺序,并且《Java虚拟机规范》允许只要不与已有属性名重复,任 何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识 的属性。为了能正确解析Class文件,《Java虚拟机规范》最初只预定义了9项所有Java虚拟机实现都应 当能识别的属性,而在《Java虚拟机规范》的Java SE 12版本中,预定义属性已经增加到29项,后文中将对这些属性中的关键的、常用的部分进行讲解。

在这里插入图片描述
在这里插入图片描述
对于每一个属性,它的名称都要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示, 而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。 一个符合规则的属性表应该满足下表中所定义的结构。
在这里插入图片描述

Code

Java程序方法体里面的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性内。 Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽 象类中的方法就不存在Code属性,如果方法表有Code属性存在,那么它的结构将如下图
在这里插入图片描述
attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,此常量值固定为“Code”,它 代表了该属性的属性名称,attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共为 6个字节,所以属性值的长度固定为整个属性表长度减去6个字节。 max_stack代表了操作数栈(Operand Stack)深度的最大值。在方法执行的任意时刻,操作数栈都 不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度。
max_locals代表了局部变量表所需的存储空间。在这里,max_locals的单位是变量槽(Slot),变量 槽是虚拟机为局部变量分配内存所使用的最小单位。对于byte、char、float、int、short、boolean和 returnAddress等长度不超过32位的数据类型,每个局部变量占用一个变量槽,而double和long这两种64 位的数据类型则需要两个变量槽来存放。方法参数(包括实例方法中的隐藏参数“this”)、显式异常处 理程序的参数(Exception Handler Parameter,就是try-catch语句中catch块中所定义的异常)、方法体中 定义的局部变量都需要依赖局部变量表来存放。注意,并不是在方法中用了多少个局部变量,就把这 些局部变量所占变量槽数量之和作为max_locals的值,操作数栈和局部变量表直接决定一个该方法的栈 帧所耗费的内存,不必要的操作数栈深度和变量槽数量会造成内存的浪费。Java虚拟机的做法是将局 部变量表中的变量槽进行重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的变量 槽可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配变量槽给各个变量使用,根据 同时生存的最大局部变量数量和类型计算出max_locals的大小。 code_length和code用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度, code是用于存储字节码指令的一系列字节流。既然叫字节码指令,那顾名思义每个指令就是一个u1类 型的单字节,当虚拟机读取到code中的一个字节码时,就可以对应找出这个字节码代表的是什么指 令,并且可以知道这条指令后面是否需要跟随参数,以及后续的参数应当如何解析。我们知道一个u1 数据类型的取值范围为0x00~0xFF,对应十进制的0~255,也就是一共可以表达256条指令。目前, 《Java虚拟机规范》已经定义了其中约200条编码值对应的指令含义,编码与指令之间的对应关系可查 阅本书的附录C“虚拟机字节码指令表”。 关于code_length,有一件值得注意的事情,虽然它是一个u4类型的长度值,理论上最大值可以达 到2的32次幂,但是《Java虚拟机规范》中明确限制了一个方法不允许超过65535条字节码指令,即它 实际只使用了u2的长度,如果超过这个限制,Javac编译器就会拒绝编译。一般来讲,编写Java代码时 只要不是刻意去编写一个超级长的方法来为难编译器,是不太可能超过这个最大值的限制的。但是, 某些特殊情况,例如在编译一个很复杂的JSP文件时,某些JSP编译器会把JSP内容和页面输出的信息归 并于一个方法之中,就有可能因为方法生成字节码超长的原因而导致编译失败。 Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法 体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整 个Class文件里,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。了解Code属性是学 习后面两章关于字节码执行引擎内容的必要基础,能直接阅读字节码也是工作中分析Java代码语义问 题的必要工具和基本技能,为此,笔者准备了一个比较详细的实例来讲解虚拟机是如何使用这个属性 的。

Exceptions

这里的Exceptions属性是在方法表中与Code属性平级的一项属性,读者不要与前面刚刚讲解完的异 常表产生混淆。Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Excepitons),也 就是方法描述时在throws关键字后面列举的异常。此属性中的number_of_exceptions项表示方法可能抛出number_of_exceptions种受查异常,每一种受 查异常使用一个exception_index_table项表示;exception_index_table是一个指向常量池中 CONSTANT_Class_info型常量的索引,代表了该受查异常的类型。
在这里插入图片描述

LineNumberTable

LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。 它并不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中使用-g:none或-g:lines 选项来取消或要求生成这项信息。如果选择不生成LineNumberTable属性,对程序运行产生的最主要影 响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来 设置断点。line_number_table是一个数量为line_number_table_length、类型为line_number_info的集合, line_number_info表包含start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源 码行号。LineNumberTable属性的结构如下
在这里插入图片描述

LocalVariableTable和LocalVariableTypeTable

LocalVariableTable属性用于描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系,它 也不是运行时必需的属性,但默认会生成到Class文件之中,可以在Javac中使用-g:none或-g:vars选项 来取消或要求生成这项信息。如果没有生成这项属性,最大的影响就是当其他人引用这个方法时,所 有的参数名称都将会丢失,譬如IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,这对程 序运行没有影响,但是会对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获 得参数值。

SourceFile和SourceDebugExtension

SourceFile属性用于记录生成这个Class文件的源码文件名称。这个属性也是可选的,可以使用Javac 的-g:none或-g:source选项来关闭或要求生成这项信息。在Java中,对于大多数的类来说,类名和文 件名是一致的,但是有一些特殊情况(如内部类)例外。如果不生成这项属性,当抛出异常时,堆栈 中将不会显示出错代码所属的文件名。这个属性是一个定长的属性,其结构如下表所示。
在这里插入图片描述

sourcefile_index数据项是指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源码文件的 文件名。为了方便在编译器和动态生成的Class中加入供程序员使用的自定义内容,在JDK 5时,新增了 SourceDebugExtension属性用于存储额外的代码调试信息。典型的场景是在进行JSP文件调试时,无法 通过Java堆栈来定位到JSP文件的行号。JSR 45提案为这些非Java语言编写,却需要编译成字节码并运 行在Java虚拟机中的程序提供了一个进行调试的标准机制,使用SourceDebugExtension属性就可以用于 存储这个标准所新加入的调试信息,譬如让程序员能够快速从异常堆栈中定位出原始JSP中出现问题的 行号。SourceDebugExtension属性的结构如下表所示。
在这里插入图片描述

其中debug_extension存储的就是额外的调试信息,是一组通过变长UTF-8格式来表示的字符串。一 个类中最多只允许存在一个SourceDebugExtension属性。

ConstantValue

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量(类 变量)才可以使用这项属性。类似“int x=123”和“static int x=123”这样的变量定义在Java程序里面是非 常常见的事情,但虚拟机对这两种变量赋值的方式和时刻都有所不同。对非static类型的变量(也就是 实例变量)的赋值是在实例构造器()方法中进行的;而对于类变量,则有两种方式可以选择:在 类构造器()方法中或者使用ConstantValue属性。目前Oracle公司实现的Javac编译器的选择是,如 果同时使用final和static来修饰一个变量(按照习惯,这里称“常量”更贴切),并且这个变量的数据类 型是基本类型或者java.lang.String的话,就将会生成ConstantValue属性来进行初始化;如果这个变量没 有被final修饰,或者并非基本类型及字符串,则将会选择在()方法中进行初始化。 虽然有final关键字才更符合“ConstantValue”的语义,但《Java虚拟机规范》中并没有强制要求字段 必须设置ACC_FINAL标志,只要求有ConstantValue属性的字段必须设置ACC_STATIC标志而已,对 final关键字的要求是Javac编译器自己加入的限制。而对ConstantValue的属性值只能限于基本类型和 String这点,其实并不能算是什么限制,这是理所当然的结果。因为此属性的属性值只是一个常量池的 索引号,由于Class文件格式的常量类型中只有与基本属性和字符串相对应的字面量,所以就算 ConstantValue属性想支持别的类型也无能为力。ConstantValue属性的结构如下表
在这里插入图片描述
从数据结构中可以看出ConstantValue属性是一个定长属性,它的attribute_length数据项值必须固定 为2。constantvalue_index数据项代表了常量池中一个字面量常量的引用,根据字段类型的不同,字面 量可以是CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、 CONSTANT_Integer_info和CONSTANT_String_info常量中的一种。

InnerClasses

InnerClasses属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将 会为它以及它所包含的内部类生成InnerClasses属性。InnerClasses属性的结构如下表
在这里插入图片描述
数据项number_of_classes代表需要记录多少个内部类信息,每一个内部类的信息都由一个 inner_classes_info表进行描述。inner_classes_info表的结构如下表
在这里插入图片描述
inner_class_info_index和outer_class_info_index都是指向常量池中CONSTANT_Class_info型常量的索 引,分别代表了内部类和宿主类的符号引用。 inner_name_index是指向常量池中CONSTANT_Utf8_info型常量的索引,代表这个内部类的名称, 如果是匿名内部类,这项值为0。 inner_class_access_flags是内部类的访问标志,类似于类的access_flags,它的取值范围如下表所示
在这里插入图片描述

Deprecated和Synthetic

Deprecated和Synthetic两个属性都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值 的概念。Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,它可以通 过代码中使用“@deprecated”注解进行设置。 Synthetic属性代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的,在 JDK 5之后,标识一个类、字段或者方法是编译器自动产生的,也可以设置它们访问标志中的 ACC_SYNTHETIC标志位。编译器通过生成一些在源代码中不存在的Synthetic方法、字段甚至是整个 类的方式,实现了越权访问(越过private修饰器)或其他绕开了语言限制的功能,这可以算是一种早 期优化的技巧,其中最典型的例子就是枚举类中自动生成的枚举元素数组和嵌套类的桥接方法 (Bridge Method)。所有由不属于用户代码产生的类、方法及字段都应当至少设置Synthetic属性或者 ACC_SYNTHETIC标志位中的一项,唯一的例外是实例构造器“()”方法和类构造器“()”方 法。Deprecated和Synthetic属性的结构非常简单,如下表
在这里插入图片描述
其中attribute_length数据项的值必须为0x00000000,因为没有任何属性值需要设置。

StackMapTable

StackMapTable属性在JDK 6增加到Class文件规范之中,它是一个相当复杂的变长属性,位于Code 属性的属性表中。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用(详见第7章字节码验证部分),目的在于代替以前比较消耗性能的基于数据流分析的 类型推导验证器。 这个类型检查验证器最初来源于Sheng Liang(听名字似乎是虚拟机团队中的华裔成员)实现为Java ME CLDC实现的字节码验证器。新的验证器在同样能保证Class文件合法性的前提下,省略了在运行 期通过数据流分析去确认字节码的行为逻辑合法性的步骤,而在编译阶段将一系列的验证类型 (Verification Type)直接记录在Class文件之中,通过检查这些验证类型代替了类型推导过程,从而大 幅提升了字节码验证的性能。这个验证器在JDK 6中首次提供,并在JDK 7中强制代替原本基于类型推 断的字节码验证器。关于这个验证器的工作原理,《Java虚拟机规范》在Java SE 7版中新增了整整120 页的篇幅来讲解描述,其中使用了庞大而复杂的公式化语言去分析证明新验证方法的严谨性,笔者在 此就不展开赘述了。 StackMapTable属性中包含零至多个栈映射帧(Stack Map Frame),每个栈映射帧都显式或隐式地 代表了一个字节码偏移量,用于表示执行到该字节码时局部变量表和操作数栈的验证类型。类型检查 验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑 约束。StackMapTable属性的结构如下表
在这里插入图片描述
在Java SE 7版之后的《Java虚拟机规范》中,明确规定对于版本号大于或等于50.0的Class文件,如 果方法的Code属性中没有附带StackMapTable属性,那就意味着它带有一个隐式的StackMap属性,这 个StackMap属性的作用等同于number_of_entries值为0的StackMapTable属性。一个方法的Code属性最 多只能有一个StackMapTable属性,否则将抛出ClassFormatError异常。

Signature

Signature属性在JDK 5增加到Class文件规范之中,它是一个可选的定长属性,可以出现于类、字段 表和方法表结构的属性表中。在JDK 5里面大幅增强了Java语言的语法,在此之后,任何类、接口、初 始化方法或成员的泛型签名如果包含了类型变量(Type Variable)或参数化类型(Parameterized Type),则Signature属性会为它记录泛型签名信息。之所以要专门使用这样一个属性去记录泛型类 型,是因为Java语言的泛型采用的是擦除法实现的伪泛型,字节码(Code属性)中所有的泛型信息编 译(类型变量、参数化类型)在编译之后都通通被擦除掉。使用擦除法的好处是实现简单(主要修改 Javac编译器,虚拟机内部只做了很少的改动)、非常容易实现Backport,运行期也能够节省一些类型 所占的内存空间。但坏处是运行期就无法像C#等有真泛型支持的语言那样,将泛型类型与用户定义的 普通类型同等对待,例如运行期做反射时无法获得泛型信息。Signature属性就是为了弥补这个缺陷而
增设的,现在Java的反射API能够获取的泛型类型,最终的数据来源也是这个属性。关于Java泛型、 Signature属性和类型擦除,在第10章讲编译器优化的时候我们会通过一个更具体的例子来讲解。 Signature属性的结构如下表
在这里插入图片描述
其中signature_index项的值必须是一个对常量池的有效索引。常量池在该索引处的项必须是 CONSTANT_Utf8_info结构,表示类签名或方法类型签名或字段类型签名。如果当前的Signature属性 是类文件的属性,则这个结构表示类签名,如果当前的Signature属性是方法表的属性,则这个结构表 示方法类型签名,如果当前Signature属性是字段表的属性,则这个结构表示字段类型签名。

BootstrapMethods

BootstrapMethods属性在JDK 7时增加到Class文件规范之中,它是一个复杂的变长属性,位于类 文件的属性表中。这个属性用于保存invokedynamic指令引用的引导方法限定符。 根据《Java虚拟机规范》(从Java SE 7版起)的规定,如果某个类文件结构的常量池中曾经出现 过CONSTANT_InvokeDynamic_info类型的常量,那么这个类文件的属性表中必须存在一个明确的 BootstrapMethods属性,另外,即使CONSTANT_InvokeDynamic_info类型的常量在常量池中出现过多 次,类文件的属性表中最多也只能有一个BootstrapMethods属性。BootstrapMethods属性和JSR-292中 的InvokeDynamic指令和java.lang.Invoke包关系非常密切,要介绍这个属性的作用,必须先讲清楚 InovkeDynamic指令的运作原理,笔者将在第8章专门花一整节篇幅去介绍它们,在此先暂时略过。 虽然JDK 7中已经提供了InovkeDynamic指令,但这个版本的Javac编译器还暂时无法支持 InvokeDynamic指令和生成BootstrapMethods属性,必须通过一些非常规的手段才能使用它们。直到 JDK 8中Lambda表达式和接口默认方法的出现,InvokeDynamic指令才算在Java语言生成的Class文件中 有了用武之地。BootstrapMethods属性的结构如下表
在这里插入图片描述
其中引用到的bootstrap_method结构如下表
在这里插入图片描述
BootstrapMethods属性里,num_bootstrap_methods项的值给出了bootstrap_methods[]数组中的引导 方法限定符的数量。而bootstrap_methods[]数组的每个成员包含了一个指向常量池 CONSTANT_MethodHandle结构的索引值,它代表了一个引导方法。还包含了这个引导方法静态参数 的序列(可能为空)。bootstrap_methods[]数组的每个成员必须包含以下三项内容: ·bootstrap_method_ref:bootstrap_method_ref项的值必须是一个对常量池的有效索引。常量池在该 索引处的值必须是一个CONSTANT_MethodHandle_info结构。 ·num_bootstrap_arguments:num_bootstrap_arguments项的值给出了bootstrap_argu-ments[]数组成员 的数量。·bootstrap_arguments[]:bootstrap_arguments[]数组的每个成员必须是一个对常量池的有效索引。 常量池在该索引出必须是下列结构之一:CONSTANT_String_info、CONSTANT_Class_info、 CONSTANT_Integer_info、CONSTANT_Long_info、CONSTANT_Float_info、 CONSTANT_Double_info、CONSTANT_MethodHandle_info或CONSTANT_MethodType_info。

MethodParameters

MethodParameters是在JDK 8时新加入到Class文件格式中的,它是一个用在方法表中的变长属性。 MethodParameters的作用是记录方法的各个形参名称和信息。 最初,基于存储空间的考虑,Class文件默认是不储存方法参数名称的,因为给参数起什么名字对 计算机执行程序来说是没有任何区别的,所以只要在源码中妥当命名就可以了。随着Java的流行,这 点确实为程序的传播和二次复用带来了诸多不便,由于Class文件中没有参数的名称,如果只有单独的 程序包而不附加上JavaDoc的话,在IDE中编辑使用包里面的方法时是无法获得方法调用的智能提示 的,这就阻碍了JAR包的传播。后来,“-g:var”就成为了Javac以及许多IDE编译Class时采用的默认 值,这样会将方法参数的名称生成到LocalVariableTable属性之中。不过此时问题仍然没有全部解决, LocalVariableTable属性是Code属性的子属性——没有方法体存在,自然就不会有局部变量表,但是对于 其他情况,譬如抽象方法和接口方法,是理所当然地可以不存在方法体的,对于方法签名来说,还是 没有找到一个统一完整的保留方法参数名称的地方。所以JDK 8中新增的这个属性,使得编译器可以 (编译时加上-parameters参数)将方法名称也写进Class文件中,而且MethodParameters是方法表的属 性,与Code属性平级的,可以运行时通过反射API获取。MethodParameters的结构如下表
在这里插入图片描述
其中,引用到的parameter结构如下表
在这里插入图片描述
其中,name_index是一个指向常量池CONSTANT_Utf8_info常量的索引值,代表了该参数的名 称。而access_flags是参数的状态指示器,它可以包含以下三种状态中的一种或多种: ·0x0010(ACC_FINAL):表示该参数被final修饰。 ·0x1000(ACC_SYNTHETIC):表示该参数并未出现在源文件中,是编译器自动生成的。 ·0x8000(ACC_MANDATED):表示该参数是在源文件中隐式定义的。Java语言中的典型场景是 this关键字

模块化相关属性

JDK 9的一个重量级功能是Java的模块化功能,因为模块描述文件(module-info.java)最终是要编 译成一个独立的Class文件来存储的,所以,Class文件格式也扩展了Module、ModulePackages和 ModuleMainClass三个属性用于支持Java模块化相关功能。 Module属性是一个非常复杂的变长属性,除了表示该模块的名称、版本、标志信息以外,还存储 了这个模块requires、exports、opens、uses和provides定义的全部内容,其结构如下表
在这里插入图片描述
其中,module_name_index是一个指向常量池CONSTANT_Utf8_info常量的索引值,代表了该模块 的名称。而module_flags是模块的状态指示器,它可以包含以下三种状态中的一种或多种: ·0x0020(ACC_OPEN):表示该模块是开放的。 ·0x1000(ACC_SYNTHETIC):表示该模块并未出现在源文件中,是编译器自动生成的。 ·0x8000(ACC_MANDATED):表示该模块是在源文件中隐式定义的。 module_version_index是一个指向常量池CONSTANT_Utf8_info常量的索引值,代表了该模块的版 本号。后续的几个属性分别记录了模块的requires、exports、opens、uses和provides定义,由于它们的结构是基本相似的,这里仅介绍其中的exports,该属性结构如下表
在这里插入图片描述
exports属性的每一元素都代表一个被模块所导出的包,exports_index是一个指向常量池 CONSTANT_Package_info常量的索引值,代表了被该模块导出的包。exports_flags是该导出包的状态指 示器,它可以包含以下两种状态中的一种或多种: ·0x1000(ACC_SYNTHETIC):表示该导出包并未出现在源文件中,是编译器自动生成的。 ·0x8000(ACC_MANDATED):表示该导出包是在源文件中隐式定义的。 exports_to_count是该导出包的限定计数器,如果这个计数器为零,这说明该导出包是无限定的 (Unqualified),即完全开放的,任何其他模块都可以访问该包中所有内容。如果该计数器不为零, 则后面的exports_to_index是以计数器值为长度的数组,每个数组元素都是一个指向常量池中 CONSTANT_Module_info常量的索引值,代表着只有在这个数组范围内的模块才被允许访问该导出包 的内容。ModulePackages是另一个用于支持Java模块化的变长属性,它用于描述该模块中所有的包,不论是 不是被export或者open的。该属性的结构如下表
在这里插入图片描述
package_count是package_index数组的计数器,package_index中每个元素都是指向常量池 CONSTANT_Package_info常量的索引值,代表了当前模块中的一个包。 最后一个ModuleMainClass属性是一个定长属性,用于确定该模块的主类(Main Class),其结构 如下表
在这里插入图片描述
其中,main_class_index是一个指向常量池CONSTANT_Class_info常量的索引值,代表了该模块的 主类。

运行时注解相关属性

早在JDK 5时期,Java语言的语法进行了多项增强,其中之一是提供了对注解(Annotation)的支 持。为了存储源码中注解信息,Class文件同步增加了RuntimeVisibleAnnotations、 RuntimeInvisibleAnnotations、RuntimeVisibleParameterAnnotations和RuntimeInvisibleParameter- Annotations四个属性。到了JDK 8时期,进一步加强了Java语言的注解使用范围,又新增类型注解 (JSR 308),所以Class文件中也同步增加了RuntimeVisibleTypeAnnotations和 RuntimeInvisibleTypeAnnotations两个属性。由于这六个属性不论结构还是功能都比较雷同,因此我们 把它们合并到一起,以RuntimeVisibleAnnotations为代表进行介绍。 RuntimeVisibleAnnotations是一个变长属性,它记录了类、字段或方法的声明上记录运行时可见注 解,当我们使用反射API来获取类、字段或方法上的注解时,返回值就是通过这个属性来取到的。 RuntimeVisibleAnnotations属性的结构如下表
在这里插入图片描述
num_annotations是annotations数组的计数器,annotations中每个元素都代表了一个运行时可见的注 解,注解在Class文件中以annotation结构来存储,具体如下表
在这里插入图片描述
type_index是一个指向常量池CONSTANT_Utf8_info常量的索引值,该常量应以字段描述符的形式 表示一个注解。num_element_value_pairs是element_value_pairs数组的计数器,element_value_pairs中每 个元素都是一个键值对,代表该注解的参数和值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值