【温故知新-Java虚拟机篇】3.类文件结构

来源:互联网 发布:千人基因组数据库 编辑:程序博客网 时间:2024/06/08 05:20

该系列博客暂且定义为《深入理Java解虚拟机》的笔记,有些坑等后续看完书再填,有不对的地方多指教。

Java 号称“一次编写,到处运行”,平台无关性是Java一个重要的特性,Sun公司以及其他虚拟机提供商发布了许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行相同一种平台无关的字节码,从而实现一次编写,到处运行。

《【回头学Java-虚拟机篇】——1.内存模型》

《【回头学Java-虚拟机篇】——2.垃圾收集器》

Java虚拟机现在已经不仅仅为Java语言服务,已经有很多其他语言运行在它的上面,如JRuby,Scala,Jython,Croovy等等。Java虚拟机只与“Class文件”这种特定的二进制格式所关联,不同的语言通过不同的编译器,按照语法和结构化约束,将代码编译成字节码文件,语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成,然后即可由虚拟机执行。

1.Class类文件的结构

1)任何一个Class文件都对应着唯一一个类或者接口的定义信息,但反过来,类和接口并不一定都定义在文件里(譬如类或接口也可以通过类加载器直接生成

2)Class文件是一组以8字节为基础单位的二进制流

3)Class文件的两种基本数据类型:

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

b.表:表是由多个无符号数或者其他表作为数据项构成的符合数据类型,所有表都习惯地以“_info”结尾。

4)Class文件格式如下,我们将一一讲解其中每项的意义:


a.1-4字节:magic,每个Class文件的头四个字节成为魔数(Magic Number),他的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件,很多文件存储的标准中都会使用魔数,而不是用扩展名来进行识别。Class文件的魔数值为0xCAFFEBABE(咖啡宝贝)。

b.5-6字节和7-8字节分别为Class文件的次版本号和主版本号:Java的版本号从45开始,JDK1.1之后的每个JDK大版本发布主版本号+1。次版本号为小版本的更新。所以JDK1.1只支持45.0~45.65535(因为次版本号是2字节,最大2^16次方,即65535)。JDK1.2同理45.0~46.65535。1.7主版本号到51.0

c.常量池:由于常量的数量是不固定的,所以在常量池的入口需要放置一个u2类型的数据,代表常量池容量计数值(constant_pool_count),它从1开始记数(0用作特殊标识),也就是说表示后面的常量池有contant_pool_count-1个常量。常量池中主要放两大类常量:

1.字面量(Literal):比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。

2.符号引用(Symbolic Reference):包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。

每种常量类型都有自己的格式,目前JDK1.7中有14中常量类型,他们有个共同的特点,即第一位是一个u1类型的标志位(tag,取值如下表“标志”):


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


e.接下来是类索引(this_class)、父类索引(super_class)以及接口索引集合(interfaces):

1.类索引和父类索引:它们都使用一个u2类型的索引值表示,指向一个类型为CONSTANT_Class_info类型的类描述常量。类描述常量中又有一个索引值指向CONSTANT_Utf8_info类型的常量,其中包含类或者父类的全限定名

2.接口索引集合:也是和其他的表一样,包含一个interfaces_count和一个interfaces集合,集合项的结构与类索引类似

f.字段表集合(filed_info):用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括方法内部声明的局部变量。可包含信息有:

  • 字段的作用于(public private protected修饰符)
  • 是实例变量还是类变量(static修饰符)
  • 可变性(final)
  • 并发可见性(volatile修饰符,是否强制从住内存读写,而不是线程内存)
  • 可否被序列化(transient修饰符)
  • 字段数据类型(基本类型、对象、数组)
  • 字段名称


其中的修饰符都可以用0-1来表示是否存在,所以他们都放在上表的access_flags中,每个修饰符一位的标志,具体如下表


修饰符后面是name_index和descriptor_index,它们都是常量池的引用,分别代表字段的简单名称以及字段和方法的描述符:

  • name_index(简单名称):指没有类型和修饰符的方法或者字段名称,如int age;  中的age,void get()中的get;
  • descriptor_index:作用是来描述字段的数据类型、方法的参数列表(包括数量、类型、顺序)和返回值。其中基本类型由一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示,数组则使用“[”字符来描述,如String[][] 记录为“[[Ljava/lang/String”

  • 使用描述符是按照先参数后返回值的方式记录,参数在括号中,如int indexOf(char[] chars, char target, int range)->“([CCI)I”
  • 字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原来Java代码中不存在的字段,譬如内部类为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

g.方法表集合(method_info):

1.方法表结构与字段表结构一致,其中access_flags中与其不同在于用于方法和字段的修饰符不尽相同。如字段volatile不能修饰方法,synchronized也不能修饰字段。方法的访问标志如下:


2.方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表(attributes)集合中一个名为“Code”的属性里面。

3.如果父类方法在子类中没有被重写(Override),方法集合中就不会出现来自父类的方法信息。但是有可能会出现编译器自动添加的方法,最经典的便是类构造器“<clinit>”方法和实例构造器“<init>”方法。

4.方法重载,除了要和原方法有相同的名字外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法各个参数在常量池中的字段符号引用的集合,返回值不会包含在内。

h.属性表集合:

1.Class文件、字段表、方法表都有自己的属性集合(attribute_info),属性表中没有严格顺序。

2.虚拟机规范中预定义的属性如下:



3.Code属性:

1)Java程序方法体中的代码经过Javac编译器处理后,最终会编程字节码指令存储在Code属性中。接口、抽象类可以没有这个属性。


2)attribute_name_index和attribute_length是每个属性都有的两个字段,其中attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为属性名,Code属性即是“Code”。attribute_length指示了属性值的长度,固定为属性表长度-这两字段长度。

3)max_stack:代表了操作数栈(Operand Stacks)深度的最大值。在方法执行任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中操作栈深度。

4)max_locals:代表局部变量所需的存储空间

a.单位为Slot,Slot是虚拟机为局部变量分配内存的最小单位,对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用一个Slot,而double和long这两种64位的数据类型则需要两位Slot存放。

b.局部变量表的存储范围:方法参数(包括实例中的this)、显式异常处理器的参数(就是try-catch中catch块所定义的异常)、方法体中定义的局部变量。

c.max_locals并不是所有局部变量所占Slot的和,当代码超出一个局部变量作用于那么这个Slot可以被重用。

5)code_length和code用于存储Java源程序编译后生成的字节码指令。code_length代表字节码长度,code用于存储字节码指令的字节流

a.字节码指令是一个u1类型的单字节,最多可有256中类型,Java虚拟机中定义了约200条。

b.虚拟机限定一个方法中最多可包含2^32-1条指令,也就是65535,超过这个长度会被拒绝编译。

6)exception_table_length和exception_table,为显式异常处理表,表结构如下:


a.start_pc,字节码开始行,end_pc:字节结束行,catch_type捕获异常的类型(类型以及子类)、handler_pc异常字节码行。

当字节码在[start_pc,end_pc)这个区间内出现catch_type的异常,则跳转到handler_pc行处理。

4.Exception属性:作用是列举出方法中可能抛出的受查异常(Checked Exception,既需要被调用者显式捕获的异常,继承于Exception类),也就是说方法描述时throws关键字后列举的异常。


5.LineNumberTable属性:用于描述Java源码与字节码行号(字节码偏移量)之间的对应关系。既某行代码的行号与字节码字节流中的行号对应。

6.LocalVariableTable属性:用于描述栈帧中局部变量和Java源代码中定义的变量之间的关系。如果取消生成这些信息,那么在调用方引用这个方法的时候,所有的参数名都会丢失,ID会添加诸如arg0,arg1这类的名称,来代替原有的参数命。

7.SourceFile属性:用于记录生成这个Class文件的源代码名称。

8.ConstantValue属性:通知虚拟机自动为静态变量赋值。

9.InnerClasses属性:用于记录内部类和宿主类之间的关联。

10.Deprecated以及Synthetic属性:二者都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值概念。

1)Deprecated用于标识某个类、字段或者方法,已经呗作者定位不再推荐使用  @Deprecated 注解。

2)Synthetic代表此字段或者方法并不是由Java代码直接产生的,而是由编译器自行添加的。

11.StackMapTable属性:类型检查器所需要的映射表,属性中包含零到多个栈映射帧,每个栈映射帧都显式或者隐式地代表了一个字节码偏移量,用于表示该执行到该字节码时,局部变量和操作数栈的验证类型。类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束。

12.Signature属性:用于记录泛型签名信息。

13.BootstrapMethods属性:用于保存invokedynamic指令引用的引导方法限定符。






原创粉丝点击