JVM(五):类文件结构

平台无关性

Java提出一个非常著名的宣传口号就是一次编写,到处运行

Java通过让虚拟机在不同的硬件平台和操作系统都可以载入和执行同一种无关的字节码,说白了只要电脑装上了JVM虚拟机,就可以去运行任何环境写出的字节码,而其他环境不同之处仅仅只是字节码编译器不同

在这里插入图片描述
可以看到,无论任何系统,通过能执行Class字节码,因为虚拟机完全将操作系统屏蔽了起来

Class类文件的结构

Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符,使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有任何空隙存在,也就是说每个Class文件的大小一定是8个字节的倍数,若遇到需要占用8个字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8个字节进行存储

Class文件采用一种类似C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型

  • 无符号数

Class的数据类型

上面提到了,Class文件采用伪结构来存储数据,这种伪结构只有两种数据类型

无符号数

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

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

类型名称数量
u4无符号数magic1
u2无符号数minor_version1
u2无符号数major_version1
u2无符号数constant_pool_count1
cp_infocontant_poolconstant_pool_count - 1
u2无符号数access_flags1
u2无符号数this_class1
u2无符号数super_class1
u2无符号数interfaces_count1
u2无符号数interfacesinterfaces_count
u2无符号数fields_count1
field_infofields1
u2无符号数methods_count1
method_infomethodsmethods_count
u2无符号数attributes_count1
attribute_infoattributesattributes_count

要强调的一点,Class的结构不像XML等描述语言,Class是没有任何的分隔符号的,所以对于表的结构,无论是顺序还是数量,甚至于数据存储的字节序这样的细节,都是被严格限定的,哪个字节代表什么含义, 长度是多少,先后顺序是什么,全部都要按约定来,不允许改变

拓展:字节序称为Byte Ording,Class文件使用的字节序为Big-Endian的排序方式,具体顺序是指高位字节在地址最低位,而最低字节在地址最高位来存储数据

魔数与Class文件的版本

每个Class文件的头4个字节被称为魔数(Magic Number),从上表可以看到其是一个U4的无符号数

魔数只有唯一一个作用,确定这个文件实赋为一个能被虚拟机接受的Class文件,也就是说起到了一个身份识别的作用,当然不只有Class文件存在魔数,比如一些GIF或者JPEG等在文件头都存在着魔数。

那为什么不直接使用扩展名来作识别呢?而是要额外使用空间呢?

这是出于安全考虑,因为扩展名可以随便地进行更改,而采用固定的魔数值可以更好识别,且没有去仔细研究过Class文件的一般不知道魔数值

而Class文件的魔数值也很有趣,值为0xCAFEBABY,寓意为咖啡宝贝

魔术后面接着的就两个u2无符号数,minor_version与major_version,这两个无符号数加起来为4个字节,存储着Class文件的版本号,首先前两个字节,即第5和第6个组成的minor_version是次版本号;而后两个字节,即第7和第8个组成的major_version是主版本号

Java的版本号是从45开始的,也就是45.0就代表了JDK1.0,但这里要注意,从JDK1.0~1.1,是使用45.0 ~ 45.3来代表版本号的,在JDK1.1之后的每个Java大版本都会进行加1;并且从中可以看出,高版本的JDK可以向下兼容一部分低版本的JDK,但不能进行向上兼容,这也是为什么IDEA无法使用JDK1.8打开JDK13的java文件,JAVA虚拟机规范了即使Class文件格式并未发生过任何改变,虚拟机也必须拒绝执行超过其版本号的Class文件

常量池

现在前8个字节都已经看了,分别是魔数和Class文件的版本,紧接着就是关于常量池的了

紧接着主次版本号之后的是常量池入口,常量池其实相当于Class文件里得资源仓库,它是Class文件结构中与其他项目关联最多的数据,通常也是占Class文件空间最大的数据项目之一,并且还是在Class文件中第一个出现的表类型数据项目

常量池的组成是由const_pool_count与const_pool组成,所以要使用表来存储

下面就来说明一下这两部分的作用

  • const_pool_count:常量池中常量的数目是不固定的,所以在常量池的入口需要设置一项u2类型的数据,使用这个u2数据代表容量池容量计数值,说白了就是用来统计常量池中的常量数目,const_pool_count一般从1而不是0开始,所以如果这个数为22,那常量数目就是21,常量池容量为什么不从0开始呢?这是因为类里面有自己的常量池,同时类也会去引用其他类的常量池,而0代表的是不引用任何一个常量池项目,也就是自身没有,也不去引用其他常量池,Class文件结构中只有常量池的容量计数是从1开始,而对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始

  • const_pool:存放常量的地方,常量池中主要存放两大类常量,字面量和符号引用

    • 字面量更接近于语言的常量概念,比如字符串、final修饰的变量

    • 而符号引用则属于编译原理方面的概念,如下几方面

      • 被模块导出或者开放的包(Package)
      • 类和接口的全限定名(Fully Qualified Name)
      • 字段的名称和描述符(Descriptor)
      • 方法的名称和描述符
      • 方法句柄和方法类型
      • 动态调用点和动态变量
    • 说白了符号引用其实就是一个连接的东西,通过解析这个连接,可以找到属性在内存中的真正地址

    • 为什么会有符号引用呢?这是因为在进行Javac编译的时候,是没有连接步骤的,而是在虚拟机去加载Class文件的时候才会进行动态连接,说白了就是Class文件不会去保存各个静态方法、字段最终在内存中的布局信息,而是保存其符号引用,如果这些符号引用不经过虚拟机在运行期转换是无法得到对应的真正内存地址,也就无法访问调用

常量池中的常量在const_pool中存放,而且每个常量都是一个表,最初的常量表中共有11种结构各不相同的表结构数据,截止JDK13,常量表中已经有17种不同类型的常量

这17类表都有一个共同的特点,表结构起始的第一位是个u1类型的标志位,这个标志位标示着这个是什么表结构(属于17种中的哪种)

下面来看看这几种的表结构

类型标志位描述
CONSTANT_Utf8_info1形容UTF-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_Packeage_info20表示一个模块中开放或者导出的包

这17种常量类型各自有着完全独立的数据结构,所以常量池是很繁琐的

CONSTANT_Class_info

这种常量结构是用来代表一个类或接口的符号引用

它的组成只有两部分

  • tag:标志位,u1类型,用于区分常量类型的,在CONSTANT_Class_info中,该标志位为7
  • name_index:符号引用,u2类型(也就是说使用两个字节来表示一个指针),它指向了常量池中的一个CONSTANT_Utf8_info类型常量,是一个常量索引,该常量代表了类或者接口的全限定名称,这里拓展一下符号引用,符号引用起始是以一组符号来描述引用的目标,这一组符号可以是任何形式的字面量,只要使用时能够无歧义的定位到目标即可,可以看到像这里的符号引用起始就是类的全限定名
CONSTANT_Utf8_info

这种常量结构是用来代表一个UTF8编码的字符串的

它的组成有三部分

  • tag:标志位,u1类型
  • length:字符串的长度(字节为单位),u2类型
  • bytes:u1类型,这里u1类型不是指整个字符串就用一个u1类型结构来存储,而是由多个u1类型来存储,数量由字符串的长度来决定,因为这个是存放使用UTF8缩略编码表示的字符串的地方

拓展:UTF8缩略编码与UTF8编码的区别

  1. 从u0001到u007f之间的字符,也就是1~127的ASCII码,缩略编码是用1个字节表示
  2. 从u0080到u07ff之间的字符,缩略编码使用2个字节表示
  3. 从u0800到uffff之间的所有字符的缩略编码就按照普通UTF8编码规则使用三个字节表示

这里要提一下,对于Class文件,所有的方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以对于这种类型常量的最大长度其实就是Java中的方法、字段名的最大长度,而限制了Java中的方法、字段名的最大长度就是CONSTANT_Utf8_info的length属性

CONSTANT_Integer_info

这种常量结构代表一个int值,组成如下

  • tag:类型为u1,标志位
  • bytes:类型为u4,按照高位在前存储的int值,这里u4是4个字节,所以int占用4个字节
CONSTANT_Float_info

这种常量结构代表一个float值,组成如下

  • tag:标志位
  • bytes:类型为u4,按照高位在前存储的float值,所以float占用4个字节
CONSTANT_Long_info

这种常量结构代表一个long值,组成如下

  • tag:标志位
  • bytes:类型为u8,按照高位在前存储的long值,所以long占用8个字节
CONSTANT_Double_info

这种常量结构代表一个double值,组成如下

  • tag:标志位
  • bytes:类型为u8,按照高位在前存储的double值,所以double占用8个字节
CONSTANT_String_info

这种常量结构代表一个字符串,这也是一个索引,指向了CONSTANT_Utf8_info的

  • tag:标志位
  • index:u2类型,指向字符串字面量的索引
CONSTANT_Fieldref_info

这种常量结构用来形容字段的,代表字段的信息,比如字段来自哪个类,名称和类型是什么

  • tag:标志位
  • index:u2类型,指向声明字段的类或者接口描述符的CONSTANT_Class_info(然后CONSTANT_Class_info又指向一个CONSTANT_Utf8_info,CONSTANT_Utf8_info存储的是符号索引)
  • index:u2类型,指向描述字段的CONSTANT_NameAndType_info
CONSTANT_Methodref_info

这种常量结构用来形容方法的,代表方法的信息,比如方法来自哪个类,名称和返回值类型是什么

  • tag:标志位
  • index:u2类型,指向声明方法的类描述符的CONSTANT_Class_info
  • index:u2类型,指向描述方法的CONSTANT_NameAndType_info
CONSTANT_InterfaceMethod-ref_info

这种常量结构也是用来形容方法的,只不过代表的是接口中的放啊

  • tag:标志位
  • index:u2类型,指向声明方法的接口描述符CONSTANT_Class_info
  • index:u类型,指向描述方法的CONSTANT_NameAndType_info
CONSTANT_NameAndType_info

这种 结构是用来形容字段或者方法的名称常量和描述符常量的

  • tag:标志位
  • index:u2类型,指向该字段或者方法名称常量项的索引
  • index:u2类型,指向该字段或者方法描述符的常量项的索引
CONSTANT_MethodHandle_info

这种结构是用来形容给方法句柄的

  • tag:标志位
  • reference_kind:该参数决定了方法句柄的类型,而句柄的类型决定了方法句柄的字节码行为
  • reference_index:常量池的一个有效索引(句柄的作用就是可以访问其他常量池)
CONSTANT_Module_info

这种结构是用来形容模块的,也就是import

  • tag:标志位
  • name_index:指向存储模块名字的CONSTANT_Utf8_info的指针,而CONSTANT_Utf8_info里面是符号引用
CONSTANT_Package_info

这种结构是形容本类的包名称的,对应的就是package

  • tag:标志位
  • name_index:指向存储包名称的CONSTANT_Utf8_info结构

访问标志

常量池之后紧接着的就是访问标志了,访问标志的作用是用于识别一些类或者接口层次的访问信息,比如这个Class是个类还是个接口;访问修饰符是什么;是否是抽象的;是不是被final修饰的

访问标志占用2个字节,对应Class里面的access_flags,类型为u2

具体的标志位如下

标志名称标志值含义
ACC_PUBLIC0x0001是否为Public
ACC_FINAL0x0010是否被声明为final,只有类可以i设置,接口不可以设置
ACC_SUPER0x0020是否允许使用invokespecial字节码指令的新语义
ACC_INTERFACE0x0200是不是一i个接口
ACC_ABSTRACT0x0400是否为abstract类型,只有抽象类和接口这个标志位才会为true
ACC_SYNTHETIC0x1000判断该类是否为用户代码产生,如果是的话就为0,不是的话就为1,也就是标识这个类并非由用户代码产生
ACC_ANNOTATION0x2000标识这是一个注解
ACC_ENUM0x4000标识这是一个枚举
ACC_MODULE0x8000标识这是一个模块

在access_flags中,其实总共有16个标志位可以使用,而直至JDK9当前只定义了9个,没有使用到的标志位要求一律为0

那么才两个字节的access_flags如何去代表那么多含义呢?

从上表中可以看到,标志名称的标志值并不是连续的,而是有一定距离的,并且该距离通常以位数来拉开,比如说我一个0x8421的access_flags,只可以代表其是一个模块、并且为abstract类型、允许使用invokespecial字节码指令、且为public

类索引、父类索引和接口索引集合

继续往后的部分就是this_class、super_class、interfaces_count和interfaces部分

  • this_class:u2类型,本类索引
  • super_class:u2类型,父类索引
  • interfaces:一组u2类型的数据的集合,接口索引集合

类索引的作用是用于确定这个类的全限定名,而父类索引则是确定这个类的父类的全限定名,由于Java对于类是不允许多继承的,所以父类索引只有一个,除了Object之外,其他的类都有父类,也就是说其他的类的父类索引都不可能为0,而接口索引则存储了该类实现了哪些接口,这些被实现的接口将会按从左到右的顺序排列在接口索引集合中

本类索引、父类索引、接口索引集合都按顺序接着访问标志进行排列

本类索引和父类索引都是u2类型的,里面存储的是指向CONSTANT_Class_info结构的索引,通过该索引是可以找到CONSTANT_Class_info,而CONSTANT_Class_info存储的是指向CONSTANT_utf8_info的索引,CONSTANT_utf8_info里面存储的正式类的全限定名称(一个符号引用)

而对于接口索引集合是分为两部分的

  • interfaces_count:一个u2类型,存储的是类实现接口的数量,称为接口计数器,同时也表示了下面索引表的容量
  • interfaces:u2类型集合,称为接口索引表,存储的也是一个指向CONSTANT_Class_info结构的索引,通过该索引是可以找到对应的CONSTANT_Class_info,而CONSTANT_Class_info存储的是指向CONSTANT_Utf8_info的索引,CONSTANT_Utf8_info里面存储的正是接口的全限定名

字段表集合

上面已经将魔数、Class文件版本、常量、访问标志和类索引、父类索引、接口索引集合排列好了,接下来到类里面自身拥有的字段了,也就是对应fields_count与fields

字段表用于描述接口或者类中声明的变量,对于字段这个概念,字段是包括类级变量和实例级变量(即静态变量和成员变量,静态变量属于类,而成员变量属于实例),但并不包括在方法内部声明的局部变量

字段可以包括下面的信息

  • 字段的作用域,即修饰符(public private protected)

  • 字段的数据类型

  • 字段的名称

  • 是实例变量还是类变量,即有没有被static关键字修饰

  • 是否可变,即有没有被final关键字修饰

  • 是否并发可见,是否强制从主存中读取,即是否被volatile关键字修饰

  • 可否进行序列化,即是否被transient修饰符修饰

对于字段的各种修饰符,对应的其实只是一个布尔值而已,有为1,没有为0,说白了其实各种修饰符对应的只是一个标志位,每个字段都拥有这个标志位;但对于字段中的名称、数据类型、是无法统一起来的,因此只能通过引用常量池的常量来进行描述

所以对于字段表,拥有以下属性

类型名称数量
u2access_flags:字段的修饰符1
u2name_index:字段的名称,指向常量池的一个常量1
u2descriptor_index:字段的描述符,指向常量池的一个常量1
u2attributes_count:额外信息的数量1
attribute_infoattributes:额外信息attributes_count
字段访问标志

对于字段访问标志,因为访问标志有限并且每个字段都有,所以采用标志位的形式,记录在access_flags中(这里的访问标志并不仅仅是权限符,还有静态statc、序列化transient、内存可见性volatile)

字段访问标志使用对应的标志值来表示

标志名称标志值含义
ACC_PUBLIC0x0001字段是否为public
ACC_PRIVATE0x0002字段是否为private
ACC_PROTECTED0x0004字段是否为protected
ACC_STATIC0x0008字段是否为static
ACC_FINAL0x0010字段是否为final
ACC_VOLIATILE0x0040字段是否为volatile
ACC_SYTHETIC0x1000字段是否由编译器自动产生(一般都是代码产生)
ACC_TRANSIENT0x0080字段是否为transient
ACC_ENUM0x4000字段是否为enum

可以看到,这里的标志值也不是连续的,比如access_flags最终组成了0x4080,它只能是为enum类型并且被transient进行修饰,而不会产生其他歧义现象

而对于接口之中的字段,都会有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志

名称与类型常量池引用

紧接着下来就是name_index与descriptor_index,这两个都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符

简单名称与描述符

之前在常量池中也有提到过简单名称与描述符,但没有具体将其含义,下面就来认识一下这两个概念

**简单名称其实是指:字段名称和没有类型和参数修饰的方法名称;**而描述符则相对复杂一些,描述符是包含详细信息的,描述符的作用是描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值

对于数据类型(这里也包括返回值的显示),有基本数据类型、引用类型和数组类型,对于基本数据类型,描述符采用大写字符来表示(这里的基本数据类型包括void),而如果是引用类型(对象类型)则是以L加对象的全限定名来并表示,详情如下表所示

标识字符含义
Bbyte类型
Cchar类型
Ddouble类型
Ffloat类型
Iint类型
Jlong类型
Sshort类型
Zboolean类型
Vvoid类型
L对象类型,比如返回值为Integer,则为Ljava/lang/Integer

而对于数组类型,每一维度都会使用一个前置的"["字符来表示,比如如果返回值为一个字符串的二维数组,那么标识字符则是[[Ljava/lang/String,但如果是一个整形数组,则是[[I

对于方法,则是按照先参数列表、后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号内,比如无参的toString方法,则为()Ljava/lang/String;括号里面的是参数,L代表返回值为引用类型,后面代表引用类型的具体类型,比如一个int(double a),对应二点形式则为(D)I

现在已经知道了简单名称与描述符,简单名称与描述符都只是对应常量池里面的CONSTANT_Utf8_info结构而已,而这里的name_index和descriptor_index是一个指向常量池的简单名称与描述符的索引

字段表中包含的数据一般在descriptor_index就结束了,但在此之后是会跟随一个属性表集合,用来存储一些额外的信息,倘若直接给一个静态变量并且设置了初值,final static int a = 123,那么属性表中可能会存在一个ConstantValue属性,其值指向了常量表中的123

这里要注意,字段表集合中不会列出从父类或者接口那里继承而来的字段,但可能出现原本代码中不存在的字段,比如在内部类中为了保持对外部类的访问性,内部类中会自动添加指向外部类的字段

方法表集合

字段存放在字段表集合中,而方法存放在方法表集合中,并且方法表的描述跟字段表的描述是一样的

类型名称数量
u2access_flags:方法的修饰符1
u2name_index:方法的名称,指向常量池的一个常量1
u2descriptor_index:方法的描述符,指向常量池的一个常量1
u2attributes_count:额外信息的数量1
attribute_infoattributes:额外信息attributes_count
字段访问标志

方法表同样也有字段访问标志,不过字段访问标志跟字段表的有点出入

  • 字段表拥有transient、volatile,但方法表没有,并且字段可能是枚举类型,但方法肯定不会是枚举类型
  • 方法表上添加了synchroniced、native、strictfp和abstract关键字可用

因此会出现一些出入

标志名称标志值含义
ACC_PUBLIC0x0001是否为public
ACC_PRIVATE0x0002是否为private
ACC_PROTECTED0x0004是否为protected
ACC_STATIC0x0008是否为static
ACC_FINAL0x0010是否为final
ACC_Synchronized0x0040是否为synchronized
ACC_SYTHETIC0x1000是否由编译器自动产生(一般都是代码产生)
ACC_VARAGES0x0080方法是否接受不定参数
ACC_BRIDGE0x4000方法是不是由编译器产生的桥接方法
ACC_STRICT0x0800方法是否为strictfp
ACC_ABSTRACT0x0400方法是不是abstract
ACC_Native0x0100方法是否为native
名称与类型常量池引用

与字段表一样,都是使用name_index,descriptor_index来指向常量池中的常量,上面已经详细说明了形式是如何的

Code

至此,方法的名称、返回值、修饰符都已经有地方存储了,那对于方法里面的代码呢?

方法里的Java代码,经过Javac编译器编译成字节码指令之后,会存放在方法属性表集合中的一个名为code的属性里面,所以方法表的属性表其实跟字段表一样,也是去存放扩展的一些额外信息的

而且,与字段表一样,来自父类的方法如果没有进行重写也是不会出现在方法表中的,或者接口中的默认方法没有进行重写也是不会出现在方法表中的

属性表集合

在字段表集合、方法表集合甚至Class文件中都有属性表这个概念,下面就来对这个属性表来分析

Class文件对其他的数据项目要求严格的顺序、长度和内容不同,而属性表没有Class文件那么严格,不要求各个属性表具有严格的顺序,甚至允许只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,属性表中的属性现在已经有29项了,下面只挑选一些关键、重要的属性讲解

首先我们要认识属性表的结构,属性表中存放着各个属性,而各个属性的结构如下

类型名称数量
u2attribute_name_index:代表属性的名称,是一个指向常量表CONSTANT_Utf8_info结构的索引1
u4attribute_length:代表属性的长度,以字节为单位1
u1info:代表属性内容,u1类型代表了内容肯定会是1个字节的倍数attribute_length
Code属性

在方法表中提到过,方法表中存储方法的代码的地方是在方法的集合表里面(注意是方法的集合表,每个方法都有自己的集合表),具体来说是集合表中的Code属性,当然如果没有方法代码的话也就没有Code属性表,一个方法对应就是一个Code属性表

下面就来看看Code属性表的结构

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2max_stack1
u2max_locals1
u4code_length1
u1codecode_length
u2exception_table_length1
exception_infoexception_tableexception_table_length
u2attributes_count1
attribute_infoattributesattributes_count

下面来说明一下这几个属性的作用

  • attribute_name_index:代表了属性的名称,指向了常量池的一个CONSTANT_Utf8_info结构的索引,里CONSTANT_Utf8_info里面存储的是固定的Code字符串
  • attribute_length:代表了属性值的长度,说白了就是集合表接下来的属性的长度,可以看到属性的名称与属性值的长度所占用的空间是6个字节(u2+u4),所以属性值的长度等于整个属性表减去6个字节(attribute_length里面就是这个值)
  • max_stack:max_stack代表了操作数栈深度的最大值,每个方法执行时,任意时刻都不会超过这个深度,虚拟机运行的时候会根据这个值去分配栈帧中的操作栈深度
  • max_locals:代表了局部变量表所需的存储空间,方法里面会有变量,这些变量就存放在临时变量表上,max_locals的单位是变量槽,变量槽是虚拟机为局部变量分配内存所使用的最小单位,对于长度不超过32位的数据类型,每个局部变量只会占用一个变量槽,这里要注意的一个地方,变量槽的数量不是方法里面的局部变量的数量来唯一确定的,操作数栈和局部变量表直接决定一个该方法的栈帧所耗费的内存,不必要的操作数栈深度和变量槽数量会造成内存的浪费,而避免浪费的主要做法就是及时回收不再使用的变量槽,当局部变量的作用域范围过了之后,该局部变量所占用的变量槽就要马上进行回收,让其他局部变量可以马上使用,所以,Java编译器会根据变量的作用域来分配变量槽给各个变量进行使用,并且根据同时生存的最大局部变量数量和类型计算出Max_locals的大小
  • code_length:代表了方法经过Java源程序编译后生成的字节码指令的长度,也是以字节为单位,u4为4个字节,理论上存储的最大值为2^32,但JVM虚拟机明确限制了一个方法不允许超过哦65535条字节质量,也就是2 ^ 16,如果超过了这个数量,Javac编译器就会拒绝编译
  • code:代表了方法经过Java源程序编译后生成的字节码指令,code存储着用于存储字节码指令的一系列字节流,字节码指令,顾名思义,就是一个指令就是一个字节码,长度就是一个字节,当JVM读到code里面的一个字节码时就可以找到字节码对应的字节码指令

Code属性可以说是Class文件中最重要的一个属性,如果将Java程序中的信息分为代码和元数据

  • 代码:方法体里面的Java代码
  • 元数据:类信息、字段信息、方法定义以及其他信息

那么Code属性就代表着代码的一部分,而Class文件另外的其他数据项来形容元数据

接下来我们使用一个简单的Java代码来看看

package offer.testSome;

/**
 * @Author: Ember
 * @Date: 2021/10/26 19:29
 * @Description:
 */
public class TestClass {
    private int m;
    public int inc(){
        return m + 1;
    }
}

使用Java -v 得到的class文件信息

在这里插入图片描述

可以看到有我们之前提到的版本、还有常量池信息

在这里插入图片描述
接下来就是code信息

这里的code在init方法里面,就是缺省的一个构造函数,但这里有一个其他的点,可以看到,args_size参数竟然为1,代表有一个参数

接下来看看自己写的inc方法

在这里插入图片描述
可以看到,里面的args_size也为1

这是为什么呢?明明两个方法都没有参数,为什么args_size为1,并且locals也给这个变量给了变量槽

这是因为this指针,在类中,我们可以在成员方法使用this指针,但我们没有给this指针一个参数的位置,所以,从这里我们可以看出this指针的实现,通过在Javac编译器编译的时候把this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时候自动传入此参数而已,因此在方法里面的code属性中,至少会有一个参数,局部变量表也会预留一个变量槽位来存放对象实例的引用,但这个情况只对成员方法有效,如果改成静态方法,是不会有的

在这里插入图片描述
可以看到,在改为static之后,args_size就为0了,这是因为static修饰后就没有this这个指针变量了

接下来的就是该方法的显示异常处理表集合了,用来存储该方法try捕捉的异常,异常表对于Code属性来说并不是必须存在的,比如上面的栗子就没有异常表生成

异常表

异常表的格式如下所示,总共包含4个字段

类型名称数量
u2start_pc1
u2end_pc1
u2handler_pc1
u2catch_type1

下面就说明一下这4个字段的含义

  • start_pc:try语句位于的行数
  • end_pc:try语句结束的行数,start_pc和end_pc形成了一个行数范围,刚好就是try语句块,只要在这范围之间出现了类型为catch_type的异常或者其子类的异常就会转到handler_pc行继续处理,注意这个行数范围不是指代码,而是指编译成的字节码指令的范围
  • handler_pc:这个是负责记录处理异常是交由第几行实现的
  • catch_type:标识捕捉的异常类型,是一个指向常量池中CONSTANT_Class_info型常量的索引,当catch_type为0时,代表任意的异常情况都会转到handler_pc去进行处理

下面就来看看这个异常表是怎样的

在这里插入图片描述
可以看到,from其实就代表start_pc、to就代表end_pc,target代表的就是handler_pc,type其实对应的是常量池的CONSTANT_Class_info,为一个 符号引用

可以看到从第0条命令到第5条命令,只要在这里出现已经Exception异常,就会交由第6条命令去执行,0~5的命令就是执行逻辑并且返回值而已

加上finally语句之后,就会变成如下这样

在这里插入图片描述
在这里插入图片描述

可以看到,多出了三个any,any代表的就是任何的异常,可以看到一个有趣的现象,我们可以0~6的命令出现了Exception异常会跳到第8条命令去处理,这里与上面不加finally最大的不同就是没有经过return的命令,接下来就是0 ~ 6的命令出现了any异常会跳到了第十五行,然后8~13行的anu异常也会跳到第15行,15 ~ 16行的any异常也会跳到第15行

也就是有三条

  • 0~6的Exception交由8处理,即try语句块
  • 0~6出现不属于Exception的异常,会交由15处理,即try语句块
  • 8~13出现异常,会交由15处理,即catch语句块
  • 15~16的异常也会交由15处理,也就是finally语句块

可能大家这里就会觉得,不是说finally语句块的return会将返回值给截断的吗?但看异常表的情况好像要出现异常才可以走到finally语句块呀,但其实finally的关键字的作用没有那么简单,下面我们仔细分析一下字节码指令

在这里插入图片描述
整个指令执行的过程

  • 获取静态值
  • 取出静态值赋予i
  • i进行自增
  • 对i进行存储
  • 在这里,本来就应该返回值了,但finally语句块导致了这里将i又变为1
  • 然后返回i
  • 接下来给是astore指令,将catch中定义的Exception赋值,并且放到变量槽2中
  • 。。。

可以看到,其实finally会将正常返回值的字节码指令都进行替换,替换成finally里面的返回指令,所以压根不会正常返回,一定会经过finally的return命令

LineNumberTable属性

LineNumberTable属性是用来描述Java源码行号与字节码指令行号之间的对应关系,它并不是运行时必须道德属性,但默认会生成到CLASS文件之中的,我们也可以同属-g:none来取消生成这项信息

如果不选择这个信息,最主要的影响就是当抛出异常的时候,堆栈中是不会显示出错的代码行号,并且在调试程序的时候,也无法按照源码来设置断点

在这里插入图片描述
可以看到,LineNumberTable将代码行号与字节码命令关联在了一起

LineNumberTable的结构如下所示

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2line_number_table_length1
line_number_infoline_number_tableline_number_table_length
  • attribute_name_index:指向一个CONSTANT_Utf8的结构,里面固定的字符串就是LineNumberTable
  • attribute_length:存储的就是字符串的长度,而且是以字节为单位
  • line_number_table_length:存储的是下面line_number_info的长度,而且也是以字节为单位
  • line_number_info:里面包含了两个数据项
    • start_pc:u2类型,里面存储的就是字节码指令的行号
    • line_number:u2类型,里面存储的是源代码的行号,也就是Java源代码的行号
    • 这两个数据项就是上图中的数据项

因为下面的属性都会出现attribute_name_index和attribute_length这两个属性,所以就不会再进行赘述了

LocalVariableTable和LocalVariableTypeTable属性

LocalVariableTable是用来描述栈帧中局部变量表的变量与Java源码中定义的变量名之间的关系,它也不是一个必要的属性,也是可以进行取消生成的,取消的后果是参数名称丢失,看到的只有arg0和arg1这些东西,也就是说占位符取代了原先的参数名,并且对于调试期间无法根据参数名称从上下文获得参数值

在这里插入图片描述

LocalVariableTable的架构如下

类型名称数量
u2attribute_name_index1
u2attribute_length1
u2local_variable_table_length1
local_variable_infolocal_variable_tablelocal_variable_table_length
  • 前两个属性不再赘述

  • local_variable_table_length:代表local_variable_table的长度,单位为字节

  • local_variable_info:存储的就是方法的参数与栈帧的关联

    • start_pc:从第几行的字节码指令行出现,也就是该局部变量的生命周期开始的字节码偏移量,对应start

    • length:代表该局部变量的生命周期的作用范围覆盖的长度,对应length

    • name_index:指向常量池中的CONSTANT_Utf8_info结构的索引,代表了参数的名字,对应name

    • descriptor_index:指向常量池中的CONSTANT_Utf8_info结构的索引,代表了该参数的描述符(前面已经说明过了名字与描述符的区别),对应Signature

    • index:代表了该变量在栈帧的局部变量表中的变量槽的位置,如果该变量的数据类型为64位时,则会占据两个变量槽,即index和Index+1两个变量槽,对应slot

拓展:关于descriptor_index,在JDK5引入泛型之后,descriptor_index其实就被改成了Signature了,也就是特征签名,对于非泛型的数据来说,描述符和特征签名描述的信息是一致的,但如果是泛型,描述符会把泛型的参数化类型给擦除掉,所以不能表示泛型,所以改用了Signature

Signature属性

Signature属性出现在属性表中,是用来支持泛型签名的、记录泛型类型的,后面再仔细研究这个伪泛型、泛型擦除等问题

Signature属性结构如下

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2signature_index1
  • signature_index:是一个指向常量池的CONSTANT_Utf8_info结构的索引,该CONSTANT_Utf8_info表示类签名或方法类型签名或字段类型签名,因为泛型可以放在类上、方法上、还有字段上
StackMapTable属性

这是一个比较复杂的变长属性,该属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器使用,目的在于代替之前比较消耗性能的基于数据流分析的类型推导验证器

这个比较复杂,以后再进行研究。。。

Exceptions属性

在属性表中,接着Code属性就是Exceptions属性了,这里的Exceptions属性与Code属性里面的异常表(Exceptions table)不是同样东西,所以不要搞混了,Exception属性是和Code属性为同一级的东西,而Exceptions table是属于Code属性的

Exceptions属性的作用是列举出方法中可能抛出的受查异常,也就是方法描述时在throws关键字后面列举的异常,结构如下所示

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2number_of_exceptions1
u2exception_index_tablenumber_of_exceptions

比如我给一个方法抛出异常

在这里插入图片描述

可以看到Exceptions的情况了

number_of_exceptions代表方法可能抛出多少种受查异常,每一种受查异常都是使用一个exception_index_table来标识,而exception_index_table是一个指向常量池中的CONSTANT_Class_info型常量的索引,可以看出是一个符号引用

SoureceFile及SourceDebugExtension属性

对应的部分如下

在这里插入图片描述
可以看到SourceFile属性仅仅只是用于记录生成这个Class文件的源码文件名称,并且这个属性也是可选的,对于大多数的类来说,类名和文件名是一致的,除了一些特殊情况,比如内部类,如果不生成这个属性的话,当抛出异常时,堆栈中将不会显示出错代码所属的文件名,并且这个属性还是一个定长的属性

类型名称数量
u2attribute_name_index1
u4attribute_length1
u1sourcefile_index1

sourcefile_index是一个指向常量池的CONSTANT_Utf8_info的索引,里面保存了文件的名称

还有一个SourceDebugExtension属性,是为了方便在编译器和动态生成的Class中加入供程序员使用的自定义内容,用于存储额外的代码调试信息

ConstantValue属性

ConstantValue是用来描述static修饰的变量的,并且是位于字段表上的,并且必须要使用static和final来同时修饰才会出现

在这里插入图片描述

因为我的JDK是ORACLE版本的,ORACLE对于ConstantValue属性的选择是如果同时使用fianl和static来修饰一个变量,并且该变量的数据类型是基本类型或者是String类型的,那么就会生成ConstantValue这个属性

比如我改成Integer类型后,ConstantValue就不会生成了,并且还会多出一个static方法出来

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
该属性的作用就是通知虚拟机自动位静态变量赋值,并且仅仅限制在基本数据类型和String类型,这是因为常量池里面仅仅支持这些基本类型,对于Oracle,如果类变量被final和static修饰,那么就会采用ConstantValue的方式赋值,如果没有final修饰,就会由Clinit方法进行赋值

ConstantValue的结构如下所示

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2constantvalue_index1
  • Constantvalue_index:代表的是一个常量池中一个字面量常量的引用,根据类型的不同,可以是CONSTANT_Long_info、CONSTANT_Integer_info等。。。。

InnerClasses属性

这个属性是用于记录内部类与宿主类之间的关联的,如果一个类中定义了内部类,那么就会生成InnerClasses属性

在这里插入图片描述
InnerClass属性的结构如下所示

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2number_of_classes1
inner_classes_infoinner_classnumber_of_classes
  • number_of_classes:代表需要记录多少个内部类信息
  • inner_classes_info:存储的内部类信息
    • inner_class_info_index:u2类型,指向了常量池的CONSTANT_Class_info结构的索引,代表了内部类的符号引用
    • outer_class_info_index:u2类型,指向了常量池的CONSTANT_Class_info结构的索引,代表了外部类的符号引用
    • inner_name_index:u2类型,指向了常量池的CONSTANT_Utf8_info结构的索引,代表了内部类的名称,如果是一个匿名内部类,这里的索引值为0
    • inner_class_access_flags:u2类型,代表了内部类的访问标志,也就是ACC_PUBLIC那些,与类的access_flags类似

Deprecated及Synthetic属性

Deprecated其实就是代表该类、该方法或者该字段是否已经被弃用,只要打上了@Deprecated注解之后就代表被弃用了

在这里插入图片描述
而Synthetic属性是代表该方法或者字段并不是由用户写的代码产生的,而是由编译器自行添加的,因为这两个属性为布尔类型的,只有有跟没有这两种情况,所以这两个属性的架构都是一样的,且非常简单,仅仅只有attribute_name_index与attribute_length

MethodParameter属性

该属性是存储方法参数名称的,而且是在JDK8之后才添加的,这是由于在Class文件中存储方法的形参名称没有任何的意义,因为给参数起什么名字对于计算机来说没有任何意义,参数的名字仅仅只在源码中进行妥当命名即可,但后面发现Class文件中没有形参的名称带来了诸多的不变,比如反射的时候不能根据Class文件来获取参数名称的

一开始方法参数的名称是存放到localVariableTable属性之中的,但后面发现却不合适,因为localVariableTable是存放在code属性里面的,只有拥有方法体才会有localVariableTable属性,那么对于抽象方法来说,是不存在方法体的,而且也可能会拥有参数的,所以localVariableTable并不合适,所以最后是创建了MethodParameter属性来专门存放形参名称,与Code是一个平级的存在,也就是每个有参方法都有MethodParameter,不会受到Code属性的干扰

看一下存放在LocalVariableTable中的形参名字
在这里插入图片描述

由于没有安装比较新的JDK,所以没看到MethodParameter的细节

MethodParameter的结构如下

类型名称数量
u2attribute_name_index1
u4attribute_length1
u1parameters_count1
parameterparametersparameters_count
  • parameters_count:形参名称的个数
  • parameter:形参名称
    • name_index:u2类型,指向常量池中的CONSTANT_Utf8_info结构的索引,里面存储着形参的名称
    • access_flags:u2类型,代表着形参的状态指示器,包含了三种状态中的一种或者多种
      • ACC_FINAL:表示该参数被final修饰
      • ACC_SYNTHETIC:表示该参数不是源代码生成的,而是编译器自动生成的
      • ACC_MANDATED:表示该参数是在源文件中隐式定义的,比如this关键字

对于Class的结构大概就先认识这么多,感觉已经足够了,本来书上还有模块化相关属性的,个人感觉不太重要

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值