JVM(五):类文件结构

  • CONSTANT_Fieldref_info

  • CONSTANT_Methodref_info

  • CONSTANT_InterfaceMethod-ref_info

  • CONSTANT_NameAndType_info

  • CONSTANT_MethodHandle_info

  • CONSTANT_Module_info

  • CONSTANT_Package_info

  • 访问标志

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

  • 字段表集合

    • 字段访问标志
  • 名称与类型常量池引用

    • 简单名称与描述符
  • 方法表集合

    • 字段访问标志
  • 名称与类型常量池引用

  • Code

  • 属性表集合

    • Code属性
    • 异常表
  • LineNumberTable属性

  • LocalVariableTable和LocalVariableTypeTable属性

  • Signature属性

  • StackMapTable属性

  • Exceptions属性

  • SoureceFile及SourceDebugExtension属性

  • ConstantValue属性

  • InnerClasses属性

  • Deprecated及Synthetic属性

  • MethodParameter属性

平台无关性


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无符号数 | magic | 1 |

| u2无符号数 | minor_version | 1 |

| u2无符号数 | major_version | 1 |

| u2无符号数 | constant_pool_count | 1 |

| cp_info | contant_pool | constant_pool_count - 1 |

| u2无符号数 | access_flags | 1 |

| u2无符号数 | this_class | 1 |

| u2无符号数 | super_class | 1 |

| u2无符号数 | interfaces_count | 1 |

| u2无符号数 | interfaces | interfaces_count |

| u2无符号数 | fields_count | 1 |

| field_info | fields | 1 |

| u2无符号数 | methods_count | 1 |

| method_info | methods | methods_count |

| u2无符号数 | attributes_count | 1 |

| attribute_info | attributes | attributes_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_info | 1 | 形容UTF-8编码的字符串 |

| CONSTANT_Integer_info | 3 | 形容整形的字面量 |

| CONSTANT_Float_info | 4 | 形容单精度浮点型的字面量 |

| CONSTANT_Long_info | 5 | 形容长整型的字面量 |

| CONSTANT_Double_info | 6 | 形容双精度浮点型的字面量 |

| CONSTANT_Class_info | 7 | 类或接口的符号引用 |

| CONSTANT_String_info | 8 | 形容给字符串类型字面量 |

| CONSTANT_Fieldref_info | 9 | 形容字段的符号引用 |

| CONSTANT_Methodref_info | 10 | 形容类中方法的符号引用 |

| CONSTANT_InterfaceMethodref_info | 11 | 形容接口中方法的符号引用 |

| CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |

| CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |

| CONSTANT_MethodType_info | 16 | 表示方法类型 |

| CONSTANT_Dynamic_info | 17 | 表示一个动态计算常量 |

| CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |

| CONSTANT_Module_info | 19 | 表示一个模块 |

| CONSTANT_Packeage_info | 20 | 表示一个模块中开放或者导出的包 |

这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_PUBLIC | 0x0001 | 是否为Public |

| ACC_FINAL | 0x0010 | 是否被声明为final,只有类可以i设置,接口不可以设置 |

| ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语义 |

| ACC_INTERFACE | 0x0200 | 是不是一i个接口 |

| ACC_ABSTRACT | 0x0400 | 是否为abstract类型,只有抽象类和接口这个标志位才会为true |

| ACC_SYNTHETIC | 0x1000 | 判断该类是否为用户代码产生,如果是的话就为0,不是的话就为1,也就是标识这个类并非由用户代码产生 |

| ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |

| ACC_ENUM | 0x4000 | 标识这是一个枚举 |

| ACC_MODULE | 0x8000 | 标识这是一个模块 |

在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,说白了其实各种修饰符对应的只是一个标志位,每个字段都拥有这个标志位;但对于字段中的名称、数据类型、是无法统一起来的,因此只能通过引用常量池的常量来进行描述

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

| 类型 | 名称 | 数量 |

| — | — | — |

| u2 | access_flags:字段的修饰符 | 1 |

| u2 | name_index:字段的名称,指向常量池的一个常量 | 1 |

| u2 | descriptor_index:字段的描述符,指向常量池的一个常量 | 1 |

| u2 | attributes_count:额外信息的数量 | 1 |

| attribute_info | attributes:额外信息 | attributes_count |

字段访问标志

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

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

| 标志名称 | 标志值 | 含义 |

| — | — | — |

| ACC_PUBLIC | 0x0001 | 字段是否为public |

| ACC_PRIVATE | 0x0002 | 字段是否为private |

| ACC_PROTECTED | 0x0004 | 字段是否为protected |

| ACC_STATIC | 0x0008 | 字段是否为static |

| ACC_FINAL | 0x0010 | 字段是否为final |

| ACC_VOLIATILE | 0x0040 | 字段是否为volatile |

| ACC_SYTHETIC | 0x1000 | 字段是否由编译器自动产生(一般都是代码产生) |

| ACC_TRANSIENT | 0x0080 | 字段是否为transient |

| ACC_ENUM | 0x4000 | 字段是否为enum |

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

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

名称与类型常量池引用

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

简单名称与描述符

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

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

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

| 标识字符 | 含义 |

| — | — |

| B | byte类型 |

| C | char类型 |

| D | double类型 |

| F | float类型 |

| I | int类型 |

| J | long类型 |

| S | short类型 |

| Z | boolean类型 |

| V | void类型 |

| 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

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

方法表集合

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

| 类型 | 名称 | 数量 |

| — | — | — |

| u2 | access_flags:方法的修饰符 | 1 |

| u2 | name_index:方法的名称,指向常量池的一个常量 | 1 |

| u2 | descriptor_index:方法的描述符,指向常量池的一个常量 | 1 |

| u2 | attributes_count:额外信息的数量 | 1 |

| attribute_info | attributes:额外信息 | attributes_count |

字段访问标志

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

  • 字段表拥有transient、volatile,但方法表没有,并且字段可能是枚举类型,但方法肯定不会是枚举类型

  • 方法表上添加了synchroniced、native、strictfp和abstract关键字可用

因此会出现一些出入

| 标志名称 | 标志值 | 含义 |

| — | — | — |

| ACC_PUBLIC | 0x0001 | 是否为public |

| ACC_PRIVATE | 0x0002 | 是否为private |

| ACC_PROTECTED | 0x0004 | 是否为protected |

| ACC_STATIC | 0x0008 | 是否为static |

| ACC_FINAL | 0x0010 | 是否为final |

| ACC_Synchronized | 0x0040 | 是否为synchronized |

| ACC_SYTHETIC | 0x1000 | 是否由编译器自动产生(一般都是代码产生) |

| ACC_VARAGES | 0x0080 | 方法是否接受不定参数 |

| ACC_BRIDGE | 0x4000 | 方法是不是由编译器产生的桥接方法 |

| ACC_STRICT | 0x0800 | 方法是否为strictfp |

| ACC_ABSTRACT | 0x0400 | 方法是不是abstract |

| ACC_Native | 0x0100 | 方法是否为native |

名称与类型常量池引用

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

Code

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

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

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

属性表集合

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

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

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

| 类型 | 名称 | 数量 |

| — | — | — |

| u2 | attribute_name_index:代表属性的名称,是一个指向常量表CONSTANT_Utf8_info结构的索引 | 1 |

| u4 | attribute_length:代表属性的长度,以字节为单位 | 1 |

| u1 | info:代表属性内容,u1类型代表了内容肯定会是1个字节的倍数 | attribute_length |

Code属性

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

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

| 类型 | 名称 | 数量 |

| — | — | — |

| u2 | attribute_name_index | 1 |

| u4 | attribute_length | 1 |

| u2 | max_stack | 1 |

| u2 | max_locals | 1 |

| u4 | code_length | 1 |

| u1 | code | code_length |

| u2 | exception_table_length | 1 |

| exception_info | exception_table | exception_table_length |

| u2 | attributes_count | 1 |

| attribute_info | attributes | attributes_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个字段

| 类型 | 名称 | 数量 |

| — | — | — |

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
的引用,但这个情况只对成员方法有效,如果改成静态方法,是不会有的**

在这里插入图片描述

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

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

异常表

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

| 类型 | 名称 | 数量 |

| — | — | — |

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-PnxdZfZC-1714863380311)]

[外链图片转存中…(img-VfHIQLYk-1714863380311)]

[外链图片转存中…(img-AOc8sNCd-1714863380312)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值