Class类文件的结构

  • jvm目录

  • 我们知道我们编写的java代码只有编译成class文件之后才能被jvm虚拟机使用,不仅如此,其他依靠与jvm虚拟机执行的语言代码一样是要编译成class文件之后才能被使用。很明显的class文件格式(也称字节码)是jvm虚拟机支持的标准的可执行的代码格式。
  • class文件格式是以(8位)字节为基础单位的二进制流。各个数据项目严格按照顺序紧凑的排列在class文件之中,之间不允许有任何分隔符,这使得整个class文件中存储的内容几乎全部都是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上的存储空间时,则会按照高位在前的方式分割成若干个8位字节进行存储。
  • 根据Java虚拟机的规范,Class文件格式采用一种类似于C语言结构体的伪结构来存储,这种伪结构只有两种数据类型:无符号数和表。
    • 无符号数属于基本数据类型,以u1、u2、u4、u8来代表1个字节,2个字节,4个字节和8个字节的无符号数。无符号数可以用来描述数字、数量值或者按照UTF-8编码格式构成的字符串值。
    • 表是由一个或多个无符号数或 其他“表”组成的复合数据类型,所有的表的表名都习惯地以“_info”结尾。表的目的很简单,是为了描述有层次关系的复杂结构。所以整个Class文件格式也是一张表,不过好像没有表名。

Class 文件格式:

数据类型 名称 数量 说明
u4 magic 1 魔数
u2 minor_version 1 次版本
u2 major_version 1 主版本
u2 constant_pool_count 1 常量池入口
cp_info constant_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 fields_count 字段
u2 methods_count 1 方法数
method_info methods methods_count 方法
u2 attributes_count 1 属性数
attribute_info attributes attributes_count 属性
表1
  1. Magic-魔数
  • 魔数唯一的作用就是确定这个文件是否为一个可以被虚拟机接受的class文件,class格式的文件有一个固定的魔数值值 0xCAFEBABE。如果不是CAFEBABE则虚拟机拒绝加载该文件,这样就可以防止加载非class文件而浪费系统资源。
  1. minor_version-次版本,major_version-主版本
  • 这两个位置用来存储当前class文件所使用的java次版本和主版本。Java的版本号是从45开始的,每个大版本号发布时主版本号加1。比如jdk1.x的版本号就是45.0~45.3,jdk7的版本号为51.0。minor_version存储小数点后的“数值”,major_version存储小数点前的整数值。同时,高版本的jdk可以支持低版本的Class文件,不能支持更高版本的Class文件,这样可以避免加载不支持的class文件。
  1. constant_pool_count-常量数目,constant_pool-常量
  • 紧接着版本信息后面就是常量池,在了解这个两个位置的作用前,我们要先了解一下什么是常量池。顾名思义,常量池就是存储这个class文件中会使用到的常量信息的集合,常量池中的类型主要分为两大类:字面量(Literal)和符号引用(Symbolic References)。字面量很好理解,如文本字符串,被声明为final的常量值等,而符号引用(Symbolic References)属于编译原理方面的概念,包括以下三类:
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符
  • 介绍完什么是常量池后我们就该了解一下class文件是如何存储这些常量信息了。首先每个class所使用到的常量数目肯定是不一致的,所以提供了一个u2的位置存储constant_pool_count(常量数目信息),也称这个位置为常量池入口。但是我们从表一中可以看出constant_pool数目却是constant_pool_count-1个,这很奇怪是不是?一开始我看着也很懵,看了其他文章的讲解更懵了。后来慢慢想明白了,它的设计是这样的,constant_pool有多个,当我们需要用到某个constant_pool的时候,我们要如何找到它呢?解决方法是给它编一个索引,第一个constant_pool的索引就是1,第二个constant_pool的索引就是2,当某个“表”里需要使用到某个常量时,只要拥有这个常量对应的索引就能按顺序遍历并查找得到它。但这里有一个问题,为什么索引是从1开始,而不是从0开始,因为索引0有其它用途,使用索引 0 来表示“不引用任何一个常量池项”的意思。很像我们java里面null的作用,这个变量不指向任何对象。constant_pool_count为22,代表一个索引为0不存在的常量,和索引为1-21的常量,所以真实的constant_pool(常量)数目只有21个。
  • 只有常量池的索引是从1开始计数,往下的其它“集合”类型的索引都是从0开始计数。
  • constant_pool的类型不是基本数据类型,而是cp_info表。而cp_info并不是一张表结构的名称,而是十几张不同结构的常量类型表的统称。这些表的相同的部分以及区分的方法在于,它们表结构的第一位都是类型为u1称为“tag”的值,根据tag值的不同来区分究竟是属于什么结构的常量表。tag值及常量类型如下表:
类型 tag值 类型描述
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 字段或方法的部分符号引用
表2
  • 我们可以举一个例子来了解一下,CONSTANT_Class_info的表结构如下:
类 型 名称 数量 说明
     u1     tag     1    
u2 name_index 1 name_index 存储的就是索引值,指向常量池中一个CONSTANT_Utf8_info类型的常量
  • 补上详细的常量表结构
  1. access_flags-访问标志
  • 在常量池结束后后是2字节的访问标志。访问标志是用来表示这个class文件是类还是接口、是否被public修饰、是否被abstract修饰、是否被final修饰等。 由于这些标志都由是/否表示,因此可以用0/1表示。 访问标志为2字节,可以表示16位标志,但JVM目前只定义了8种,未定义的直接写0。具体使用到的标志位及标志含义见下表:
标志名称 标志值 含 义
ACC_PUBLIC 0x0001 是否为public类型
ACC_FINAL 0x0010 是否被声明为final
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令,jdk1.2之后编译出来的类这个标志为真
ACC_INTERFACE 0x0200 标识这是一个接口
ACC_ABSTRACT 0x0400 是否为是abstract类型,抽象类和接口,此标志为真,其它为假
ACC_SYNTHETIC 0x1000 标识这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举
  1. this_class-类索引
  • 类索引是为了用来查找当前class文件所表示类的全限定名。2个字节里面的值就是一个索引值,指向常量池里面一个CONSTANT_Class_info类型的常量。
  1. super_class-父类索引
  • 同类索引类似,为了用来查找当前class文件所表示类的父类的全限定名。我们知道除了java.lang.Object类之外,其它的类全都会有父类,所以只有java.lang.Object类文件里面的这个u2值为0。
  1. interfaces_count-接口数量,interfaces-接口索引
  • 同类索引,父类索引一样的用法,但是我们知道一个类或接口可以集成的接口有多个,所以我们需要一个interfaces_count记录当前类或接口继承的接口数。
  • 还有一点是interfaces同样是有自己的索引的,我们使用到的时候同样是需要根据索引查找到对应的接口全限定名,但是interfaces的索引是按顺序从0开始编号。
  1. fields_count-字段数目,fields-字段
  • 同接口索引集合类似,相同的我就不多复述了。field_info表是用于描述类或接口中声明的变量,但仅限于类级变量实例级变量,不包括方法内部声明的变量
  • 字段(field_info)表的结构:
类 型 名称 数量 说明
u2 access_flags 1 字段修饰符放在access_flags项目中,它与类中的access_flags项目是非常相似的
u2 name_index 1 存储常量池的某个常量的索引,代表着字段的名称
u2 descriptor_index 1 存储常量池的某个常量的索引,代表着字段的描述符
u2 attributes_count 1
attribute_info attributes attributes_count
  • 字段的access_flags值含义:
标志名称 标志值 含 义
ACC_PUBLIC 0x0001 是否为public类型
ACC_PRIVATE 0x0002 是否为private类型
ACC_PROTECTED 0x0004 是否为protected类型
ACC_STATIC 0x0008 是否为static
ACC_FINAL 0x0010 是否为final
ACC_VOLATILE 0x0040 是否为volatile
ACC_TRANSTENT 0x0080 是否为transient类型
ACC_SYNCHETIC 0x1000 字段是否为由编译器自动产生
ACC_ENUM 0x4000 是否为enum类型
  • name_index,descriptor_index是指向CONSTANT_Utf8_info类型的常量。但这里我不太明白,明明这里只需要一个u2类型指向一个CONSTANT_Fieldref_info或CONSTANT_NameAndType_info类型的常量不就行了么?这样设计是基于什么考虑的呢?
  • attributes同类中attributes表一致,用于描述某些场景专有的信息,不复述了。
  • 补充上面提到的问题,CONSTANT_Fieldref_info是用于指向fields的符号引用,我之前是本末倒置理解错了。也就是说fields是代表这个类的某个字段的信息记录,而CONSTANT_Fieldref_info是找到这个类的这个变量的入口(符号引用)
  1. methods_count-方法数目,methods-方法
  • 同接口索引集合类似,相同的我就不多复述了。
  • 方法(method_info)表结构:
类 型 名称 数量 说明
u2 access_flags 1 方法修饰符放在access_flags项目中,它与类中的access_flags项目是非常相似的
u2 name_index 1 存储常量池的某个常量的索引,代表着方法的名称
u2 descriptor_index 1 存储常量池的某个常量的索引,代表着方法的描述符
u2 attributes_count 1
attribute_info attributes attributes_count
  • 方法的access_flags值含义:
标志名称 标志值 含 义
ACC_PUBLIC 0x0001 是否为public类型
ACC_PRIVATE 0x0002 是否为private类型
ACC_PROTECTED 0x0004 是否为protected类型
ACC_STATIC 0x0008 是否为static
ACC_FINAL 0x0010 是否为final
ACC_SYHCHRONRIZED 0x0020 是否为synchronized
ACC_BRIDGE 0x0040 方法是否是由编译器产生的方法
ACC_VARARGS 0x0080 方法是否接受参数
ACC_NATIVE 0x0100 是否为native
ACC_ABSTRACT 0x0400 是否为abstract
ACC_STRICTFP 0x8000 是否为strictfp
ACC_SYNTHETIC 0x1000 方法是否是有编译器自动产生的
  • name_index,descriptor_index是指向CONSTANT_Utf8_info类型的常量。
  • attributes同类中attributes表一致,用于描述某些场景专有的信息,不复述了。
  • 方法里的代码呢?方法的参数?好像这里并没有提到,其实方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性(attribute_info)表集合中一个名为"Code"的属性里面,这我们在第10小结中再提到。
  1. attributes_count-属性数目,attributes-属性
  • 结构同上,但是属性表是什么呢?其实在方法表(method_info)和字段表(field_info)中都有存在属性表,它的作用是用于描述某些场景专有的信息。比如我们这个Class是个内部类,这些属性信息就是存储在属性表里的。
  • 属性表集合的限制没有那么严格,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,但Java虚拟机运行时会忽略掉它不认识的属性。Java虚拟机规范中预定义了9项虚拟机应当能识别的属性(JDK1.5后又增加了一些新的特性,已经有20多个了,但下面9项是最基本也是必要,出现频率最高的),如下所示:
    • Code
    • ConstantValue
    • Deprecated
    • InnerClasses
    • Synthetic
    • SourceFile
    • Exceptions
    • LineNumberTable
    • LocalVariableTable
  • 属性表(attribute_info)结构:
类 型 名称 数量 说明
u2 attribute_name_index 1 常量池的索引,该常量存储属性名,比如code
u2 attribute_length 1 属性长度
u1 info attribute_length 属性内容
  • attribute_length的长度不一定是u2也有可能是u4,比如如果是Code属性时。
    attribute_length就为u4长,Exceptions属性attribute_length就为u2

  • 我觉得attribute_length是这样用的,Code、Exceptions这些类型的属性其实都有了完整的结构,比如下方的Code属性结构,虚拟机可以识别到该属性类型,所以attribute_length对于这些定义好了的属性没有多大用处。但是我们可以自定义属性,自定义属性的时候,虚拟机并不知道也不关心info项内包含什么内容,但是虚拟机要能正确解析整个Class文件就必须知道info的长度。所以attribute_length主要是用于自定义属性的边界限定用,且大小固定为u2。

  • 下面我们介绍一下code属性的结构

CODE属性
  • Java程序方法体中的代码经过Javac编译处理后,最终变为字节码指令存储在Code属性中.Code属性出现在方法表的属性集合中,但是并非所有的方法表都有这个属性.例如接口或类中的方法就不存在Code属性了.
类 型 名称 数量 说明
u2 attribute_name_index 1 常量池的索引,该常量存储属性名,比如CODE
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 属性
  • max_stack,max_locals参考JAVA虚拟机结构之栈帧,编译阶段就能确定的值,不复述了。
  • code存储的是Java源程序编译后生成的字节码指令,具体例子也可以参照JAVA虚拟机结构之栈帧。code用于存储字节码指令的一系列字节流,它是u1类型的单字节,因此取值范围为0x00到0xFF,那么一共可以表达256条指令,目前,Java虚拟机规范已经定义了其中200条编码值对应的指令含义。
  • code_length虽然是一个u4类型的长度值,理论上可以达到2^32-1,但是虚拟机规范中限制了一个方法不允许超过65535条字节码指令,如果超过了这个限制,Javac编译器将会拒绝编译。
  • exception_table存储的是显式异常处理表,也就是代码中try…catch的相关信息,不是code属性必须的。
  • code属性自己可能还拥有其他的属性(attributes),大概是这样子吧。。。。算了算了,不想看了

参考
  • https://www.cnblogs.com/lrh-xl/tag/JVM/

你可能感兴趣的:(jvm虚拟机)