JVM理论:(三/1)class类文件结构

  各种不同平台的虚拟机,与所有平台都统一使用的程序存储格式——字节码,是构成平台无关性与语言无关性的基石。

  Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联。Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符。

  

  Class文件里的数据项无论顺序还是数量已被严格限定,如上图所示,先看类型这列,从上图可以看出Class文件只有两种数据类型,其中的u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节,这些是无符号数,可以描述数字、索引引用、数量值或按照UTF-8编码构成字符串值,无符号数属于基本的数据类型。而像cp_info、field_info等的这些类型被称为表,表是由多个无符号数或其他表构成的复合数据类型,都习惯性以“_info”结尾。

  当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,例如constant_pool_count+constant_pool共同描述的是常量池,用表示之后有constant_pool_count个连续的constant_pool。接下来一一介绍每个属性。

  winhex软件可以打开class字节码,下载链接:https://download.csdn.net/download/shenweis/4034852

1、魔数与Class文件的版本

  magic:每个Class文件的头4个字节称为魔数,它的唯一作用是确定这个文件是否为一个能被虚拟机接收的Class文件,class文件的魔数值为0xCAFEBABE

  紧接着魔数的4个字节存储的是Class文件的版本号,5到6字节是次版本号,7到8字节是主版本号,Java的版本号从45开始,高版本的JDK能向下兼容以前版本的Class文件,但不能向上兼容,例如JDK1.2.2的十进制版本号是46.0,则能支持45.0~46.0的Class文件。

 2、常量池

  constant_pool_count:常量池中常量的数量是不固定的,所以在常量池入口需要放置一向u2类型的容量计数值,表示之后有多少常量,这个计数值是从1而不是0开始,即如果constant_pool_count为22,则表示常量池里有21个常量,第0项用于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义。不过Class文件中只有常量池容量计数从1开始,其他都从0开始计数。

  当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址中。

  常量池主要存放两大类常量:

  字面量 —— 如文本字符串、声明为final的常量值等。

  符号引用 —— 类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。

  针对以上两大类,常量池中的常量又可以细分为14种类型的表,如下图。这14种表都有一个共同的特点,表开始的第一位是一个u1类型的标志位tag,用来表示当前常量属于14种类型中的哪种类型。

  

  常量池结构很繁琐,因为这14种常量各自均有自己的结构,如下图所示

   

  

  值得一提的是,Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以CONSTANT_Utf8_info型常量的最大长度也就是Java中方法、字段名的最大长度,而CONSTANT_Utf8_info的最大长度就是length的最大值,即u2两个字节,两字节的16位二进制码可以表示的最大值为0xffff,即15*(16^3)+15*(16^2)+15*16+15=65535,所以Java程序中如果定义了超过64KB英文字符的变量或方法名,将会无法编译。CONSTANT_Utf8_info型常量结构如下图。

  

3、访问标志

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

  access_flags中一共有16个标志位可以使用,当前只定义了其中8个,没有使用到的标志位要求一律为0。

  

  16进制看的可能有些疑惑,转换成2进制就清楚多了。

  

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

    Class文件通过这三项数据来确定这个类的继承关系。

  类索引(this_class),用于确定这个类的全限定名。引用一个u2类型的索引值,都是先指向一个类型为CONSTANT_Class_info的类描述符常量,再通过CONSTANT_Class_info类型常量中的索引值找到定义在CONSTANT_Utf8_info类型常量中的全限定名字符串。如下图。

  

  父类索引(super_class),用于确定这个类的父类的全限定名,Java不允许多重继承,所以父类索引只有一个,除了java.lang.Object外,所有Java类都有父类,因此父类索引都不为0。也是引用一个u2类型的索引值,先指向一个类型为CONSTANT_Class_info的类描述符常量,再通过CONSTANT_Class_info类型常量中的索引值找到定义在CONSTANT_Utf8_info类型常量中的全限定名字符串。

  接口索引集合(interfaces),用来描述这个类实现了哪些接口,入口的第一项是一个u2类型的接口计数器,表示接着有多少个u2类型的接口索引,这些被实现的接口将按implements语句后的接口顺序从左到右排列在接口索引集合中(如果这个类本身是一个接口,则应当是extends语句)。如果该类没有实现任何接口,则计数器为0,后面的接口索引集合也不再占用任何字节。

5、字段表集合

  顾名思义字段表(field_info)用于记录接口或类中声明的变量的信息,注意是这些字段只包括类级变量、实例级变量,是不包括方法内部声明的局部变量的。这些被记录下来的信息包括:字段的作用域(public、private、protected)、是实例变量还是类变量(static)、可变性(final)、并发可见性(volatile、是否强制从主内存读写)、可否被序列化(transient)、字段数据类型(基本类型、对象、数组)、字段名称。

  字段表集合不会列出从父类或父接口中继承的字段,Java中字段是无法重载的,两个字段必须是不一样的名称(但字节码中只要两字段描述符不一致就合法)。

  字段信息入口也是用一个u2类型的容量计数器fields_count来说明这个类有多少个字段,一个字段表的结构如下。

  

(1)字段访问标识access_flags也是u2的数据类型,占16位,如下图。

   

  实际情况中,类或接口中的类级或实例级变量有以下限制

  • ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志只能选其一;
  • ACC_FINAL、ACC_VOLATILE不能同时选择;
  • 接口中的字段必须有 ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志。

(2)name_index、descriptor_index,二者都是对常量池的引用

  name_index代表简单名称,简单名称是指没有类型和参数修饰的方法或者字段名称。

  descriptor_index代表字段的描述符,描述符用来描述字段的数据类型、方法的参数列表(包括数量、类型、以及顺序)和返回值。

  基本数据类型(byte、char、double、float、int、long、short、boolean)、void、对象类型的描述符如下图。

  

  对象类型的描述符是用L + 对象的全限定名表示,例如String类型的描述符为Ljava/lang/String;。

  数组类型的描述符,每一个维度用一个前置的“[”字符来描述,例如String [][]类型的描述符为[[Ljava/lang/String;,int []的描述符为[I。

    用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”内,例如,方法void inc()的描述符为()V,方法String toString()的描述符为()Ljava/lang/String;,方法int indexOf(char[] source, int index)的描述符为([CI)I。 

实例讲解:

  结合上图与代码TestClass.class 文件而言,字段表集合从地址 0x000000F8开始:

  • 第一个u2类型的数据为容量计数器fields_count,其值为0x0001,说明这个类只有一个字段表数据。
  • 接下来紧跟的是 access_flags标志,其值为0x0002,代表private修饰符的ACC_PRIVATE标志位为真(ACC_PRIVATE标志的值为0x0002) 。
  • 代表字段名称的 name_index值为0x0005,查出第5项常量是一个 CONSTANT_Utf8_info类型的字符串,其值为“m”。
  • 代表字段描述符的 descriptor_index 的值为0x0006,指向常量池的字符串“I”。

  根据以上信息,可以推断出原代码定义为“private int m;”。

(3)attributes

  属性表集合用于存储一些额外的信息,字段都可以在属性表中描述零至多项的额外信息,单独作为一点总结。

类变量、成员变量、局部变量这三个分别是在内存的哪分配的?

6、方法表集合

   Class文件存储中对方法的描述与对字段的描述类似。方法表结构如下图。

  如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息。

  

(1)access_flags,方法访问标志如下图。

   

(2)name_index、descriptor_index,与字段表类似,二者都是对常量池的引用,name_index是方法名的常量池引用,descriptor_index是方法的描述符。

(3)attributes,方法的属性表。

  方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性表里,属性表在第7点描述。

实例讲解  

   

  • 第一个u2 类型的数据(即计数器容量)的值为0x0002,代表集合中有两个方法(这两个方法是编译器添加的实例构造器<init>和源码中的方法inc())。
  • 第一个方法的访问标识值是0x001,也就是只有ACC_PUBLIC 标志为真。
  • 名称索引为0x0007,查找常量池表可知对应的方法名是“<init>”。
  • 描述符索引值为 0x0008,对应常量为“()V”。
  • 属性表计数器 attributes_count的值为0x0001,表示此方法的属性表集合有一项属性。
  • 属性名称索引为 0x0009,对应常量为“Code”,说明此属性是方法的字节码描述。

 7、属性表集合

  前面讲过的Class文件、字段表、方法表都出现过属性表集合,各个属性表的顺序不再严格要求。

  属性表的结构除了attribute_name_index和attribute_length这两部分是固定的,一共6字节,不同属性结构不同,所以属性值的长度固定为整个属性表长度减去6个字节。属性表整体结构如下图。

  attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,代表了该属性的名称;attribute_length表示属性值的长度

  

Class文件、字段表、方法表都有自己的属性表,接下来分别举例

先介绍两个属于方法表中的属性

(1)Code属性

  Java程序方法体的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。但并非所有方法都必须存在这个属性,像在接口或抽象类中的方法不存在Code属性。Code属性表结构如下图。

  Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个Class文件中,Code属性就用于描述代码,其他数据项都用于描述元数据。

  

  max_stack代表了操作数栈深度的最大值,虚拟机运行时需要根据这个值来分配栈帧中的操作栈深度。

  max_locals代表了局部变量表所需的存储空间,单位是Slot,Slot是虚拟机为局部变量分配内容所使用的最小单位,对于byte、char、flaot、int、short、boolean和returnAddress 等长度不超过32位的数据类型,每个局部变量占用1个Slot;而double和long这两种64位的数据类型则需要两个Slot来存放。方法参数、显示异常处理参数、方法体中的局部变量都需要使用局部变量表来存。局部变量表中的Slot可以重用,当代码执行超过局部变量的作用域时,Solt可以被其他局部变量使用。

    code_length 和 code:用来存储java 源代码编译后生成的字节码指令。code_length代表方法内存储字节码的长度,虽然它是一个u4类型的长度值,但虚拟机中明确限制了一个方法不允许超过65535条字节码指令,即它实际上只使用了u2的长度,如果超过这个限制, javac编译器会拒绝编译。每个code相当于一个字节码指令,是一个u1类型的单字节,u1的取值范围是0x00~0xFF,对应十进制为0~255,也就是一共最多能表达256条指令,目前Java虚拟机规范已经定义了其中约200条字节码指令,通过“虚拟机字节码指令表”可以查找对应编码与指令的对应关系。

  exception_table,这个方法的显示异常处理表集合,异常表对于Code属性来说并不是必须存在的,格式如下。如果当字节码在第start_pc行到第end_pc行之间(不含end_pc行)出现了类型为catch_type或者其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的引用),则转到第handler_pc行继续处理。当catch_type的值为0时,代表任意异常情况都需要转向到handler_pc进行处理。用于try-catch-finally机制。 

   

(2)Exceptions属性

    这里的Exceptions属性是在方法表中与Code属性平级的一项属性,而不是Code属性表中的异常属性表。是方法在throws关键字后面列举的异常。结构如下图。

  表示可能抛出number_of_exceptions种受检查异常,每一种受检查异常使用一个exception_index_table项表示,为指向常量池中CONSTANT_Class_info型常量表的索引,代表了该受检查异常的类型。

  

(3)LineNumberTable属性  

  用于描述Java源代码行号与字节码行号(字节码偏移量)之间的对应关系。Code属性里的属性。

(4)LocalVariableTable属性

  用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,它不是运行时必须的属性,如果没有生成这项属性,最大的影响就是当其它人引用这个方法时,所有参数名称都丢失,IDE可能会使用诸如arg0、arg1之类的占位符来替换原有的参数名称。Code属性里的属性。

(5)SourceFile属性

  用于记录这生成这个Class文件的源码文件名称。类文件里的属性。

(6)ConstantValue属性

  用于通知虚拟机自动为静态变量赋值。只有被static关键字修饰的变量才可以使用这项属性。字段表的属性。

  对于实例变量的赋值是在实例构造器<init>方法中进行的;对于类变量,则有两种式可以选择:赋值在类构造器<clinit>方法中进行,或者使用ConstantValue属性来赋值。目前Sun Javac编译器的选择是:如果同时使用final和static来修改一个变量,并且这个变量的数据类型是基本类型或java.lang.String的话,就生成ConstantValue属性来进行初始化,如果这个变量没有被final修饰,或者并非基本类型或字符串,则选择在<client>类构造器中进行初始化。

  有ConstantValue属性的字段必须设置ACC_STATIC标志,且ConstantValue属性值只能限于基本类型和String,有这个属性,则在类加载过程的准备阶段,会用ConstantValue中的值来初始化,而不是用默认的值来进行初始化。ConstantValue属性表结构如下:

  

  ConstantValue属性是一个定长属性,它的attribute_length数据项值必须为2。constantvalue_index数据项代表了常量池中一个字面常量的引用,根据字段类型不同,字面量可以是CONSTANT_Long_info,CONSTANT_Float_info,CONSTANT_Double_info,CONSTANT_Integer_info和CONSTANT_String_info常量中的一种。

(7)InnerClasses属性

  用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那么编译器将会为它及它所包含的内部类生成InnerClasses属性表。类文件的属性。

(8)Deprecated及Synthetic属性

  Deprecated及Synthetic属性都属性于标志类型的布尔值属性,只存在有和没有的区别,没有属性值的概念。Deprecated属性用于表示某个类,字段或方法,已经被程序作者定为不再推荐使用,它可以通过代码中使用@Deprecated注解进行设置。Synthetic属代表此字段或方法并不是由Java源码直接产生的,而是由编译器自行添加的。

(9)Signature属性

  可以出现在类、字段表、方法表的属性。记录类、接口、初始化方法或成员的泛型类型,Java语言的泛型采用的是擦除法实现的伪泛型。在字节码(code属性)中,泛型信息编译之后都通通被擦除掉,好处是实现简单、运行期能节省一些类型所占的内存空间,坏处是无法将泛型类型与用户定义的普通类型同等对待,例如运行期做反射时无法获得泛型信息。

  为了弥补这个缺陷JDK1.5后专门增设的Signature属性,现在Java的反射API能够获取泛型类型,最终的数据来源就是这个属性。Signature结构如下。

   

  signature_index值必须是一个对常量池的有效索引,且必须是CONSTANT_Utf8_info结构,如果当前的Signature属性是类文件的属性,则这个结构表示类签名,如果当前的Signature属性是方法表的属性,则这个结构表示方法类型签名,如果当前的Signature属性是字段表的属性,则这个结构表示字段类型签名。

参考链接:

  关于哪些字面量会进入常量池的测试https://www.jianshu.com/p/d8492e748c57

   https://blog.csdn.net/ITermeng/article/details/75194686

   https://blog.csdn.net/laiwenqiang/article/details/42043857

   http://gityuan.com/2015/10/17/jvm-class-instruction/

   https://www.cnblogs.com/lrh-xl/p/5350612.html

   https://blog.csdn.net/u012715840/article/details/72792623

   https://blog.csdn.net/zhangqix/article/details/53454406?utm_source=debugrun&utm_medium=referral

猜你喜欢

转载自www.cnblogs.com/zjxiang/p/9218142.html