[深入理解JVM 三]---类Class文件结构

来源:互联网 发布:linux socket服务器 编辑:程序博客网 时间:2024/05/23 01:58

本篇为深入理解JVM的第三篇,通过理解类文件结构,为之后的类加载提供一种可以理解的数据结构。说到深入,其实也算不上深入,只是有个大概理解,为类加载提供知识基础,之后用到更加详细知识需要再补充—TML
开篇之前我们首先要了解字节码文件class,它可以说是无关性的基石,通过它这样一个中间文件,java不光可以实现平台无关,也可以逐步实现语言无关。

语言无关性:不同的语言通过各自特定的编译器可以统一编译成class文件

这里写图片描述

平台无关性:有了统一的class文件,通过使用针对不同平台开发的jvm虚拟机

这里写图片描述
那么了解类文件结构就非常有必要。

Class的数据结构

Java虚拟机规定,Class类文件格式采用类似C语言结构体的伪结构来存储,这种伪结构中只有两种数据类型:无符号数和表:

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

(2).表
由多个无符号数或其他表作为数据项构成的复合数据类型,所以表都习惯性以“_info“结尾。表用于描述有层次关系的复合结构数据,整个Class文件本质就是一张表。

这里写图片描述

Class的类文件结构

按照我对类文件的理解,我认为可以分为以下几个部分,第一部分是类文件的信息,第二部分是个资源库,第三部分是描述类级别的信息,第四部分是描述字段级别的信息,第五部分是描述方法级别的信息,第六部分是描述某些场景特有信息的

第一部分:描述类文件信息

魔数与Class文件版本(magic)

每个Class文件的头4个字节被称为魔数,它的唯一作用是用于确定这个文件是否为一个能被java虚拟机所接收的Class类文件,即用于判定文件是否是符合规范的java Class文件。虽然说后缀名“.class”可以表明文件是一个Class文件,但是文件后缀名是可以随意被改动的,基于安全的考虑,很多文件都通过魔数值来唯一确定文件类型,java的Class文件魔数是:0xCAFEBABE.

版本序号(minor_version和major_version)

每个Class文件的第5和第6个字节代表Class文件的次版本号,第7和第8个字节代表Class文件的主版本号。Class文件的主、次版本号是由JDK决定的,JDK1.0~JDK1.1使用了45.0~45.3的版本号(45是主版本序号,点”.“之后的是次版本号),从JDK1.1开始,每个大版本的JDK主版本号加。Class主、次版本号的一个作用时,高版本的Java虚拟机可以向前兼容,运行低版本JDK编译的Class字节码文件,而低版本的java虚拟机不能运行高版本JDK编译的Class字节码文件

例如,JDK 1.1能支持版本号为45.0~45.65535的Class文件,无法执行版本号为46.0以上的Class文件,而JDK 1.2则能支持45.0~46.65535的Class文件。现在,最新的JDK版本为1.7,可生成的Class文件主版本号最大值为51.0。

可以清楚地看见开头4个字节的十六进制表示是0xCAFEBABE,代表次版本号的第5个和第6个字节值为0x0000,而主版本号的值为0x0032,也即是十进制的50,该版本号说明这个文件是可以被JDK 1.6或以上版本虚拟机执行的Class文件
这里写图片描述
默认文件版本号对照表
这里写图片描述

JDK1.0~JDK1.1使用了45.0~45.3的版本号,JDK1.2使用了46.0~46.65535的版本号,JDK1.3使用了47.0~47.65535的版本号,JDK1.4使用了48.0~48.65535的版本号,JDK1.5使用了49.0~49.65535的版本号,JDK1.6使用了50.0~50.65535的版本号,JDK1.7使用51.0~51.65535的版本号。

在编译时可以通过指定-target参数来改变主版本号,如JDK1.6编译时如果没有给定target参数,则编译出来的Class文件的主版本号是50,如果给定”-target 1.4 -source 1.4”参数之后,则主版本将变为48,如果给定”-target 1.5 ”参数之后,则主版本将变为49

第二部分:资源库

举例说明:拿字段来说,一个字段在虚拟机运行时从常量池获得一个符号引用,再在类解析时将符号引用转换为直接引用来获取内存地址入口例如0X0002,然后通过该地址去常量池中对应地址的位置找到获取的信息,例如字段的name_index解析后地址为0x0005,在常量表中对比后发现该索引地址指向CONSTANT_Utf8_info存放的字符串为“m”,所以得到字段名称为m。

常量池(constant_pool_count和constant_pool)

constant_pool_count代表Class文件中常量池的数目由于常量池的计数从1开始,因此常量池的容量是constant_pool_count-1。第0项常量空出做特殊考虑,为了满足一些指向常量池的索引值在某些特定情况下需要表达“不指向任何一个常量池”的意思。

它是Class文件中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一。

常量池表结构

常量池中的每一项常量都是一个表,共有11种(JDK1.7之前)结构各不相同的表结构数据,每种表开始的第一位是一个u1类型的标志位(1-12,缺少2),代表当前这个常量属于的常量类型。

这里写图片描述

这11种常量类型各自均有自己的结构。在CONSTANT_Class_info型常量的结构中有一项name_index属性,该常量属性中存放一个索引值,指向常量池中一个CONSTANT_Utf8_info类型的常量,该常量中即保存了该类的全限定名字符串。而CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info型常量的结构中都有一项index属性,存放该字段或方法所属的类或接口的描述符CONSTANT_Class_info的索引项。另外,最终保存的诸如Class名、字段名、方法名、修饰符等字符串都是一个CONSTANT_Utf8_info类型的常量,也因此,Java中方法和字段名的最大长度也即是CONSTANT_Utf8_info型常量的最大长度,在CONSTANT_Utf8_info型常量的结构中有一项length属性,它是u2类型的,即占用2个字节,那么它的最大的length即为65535。因此,Java程序中如果定义了超过64KB英文字符的变量或方法名,将会无法编译。

这里写图片描述

常量池中存储的内容

常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于Java层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用总结起来则包括了下面三类常量:

  • 类和接口的全限定名(即带有包名的Class名,如:org.lxh.test.TestClass)
  • 字段的名称和描述符(private、static等描述符)
  • 方法的名称和描述符(private、static等描述符)

  • 虚拟机在加载Class文件时才会进行动态连接,也就是说,Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中

符号引用和直接引用

  • 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。
  • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同(所以加载器采用双亲委派模式)。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。

部分分隔符================TML

第三部分:描述类的信息

访问标志(access_flags)

用于表示Class或接口层次的访问标志,即类或接口层面的访问控制信息,通常存储的信息包括:Class类文件是类、接口、枚举或是注解是否定义为public类型是否定义为abstract类型类是否被定义为final等等。
这里写图片描述

类索引,父类索引与接口索引集合

this_class类索引用于确定类的全限定名,super_class父类索引用于确定父类的全限定名,interfaces接口索引用于确定接口的全限定名,由于java中可以实现多个接口,因此使用interfaces_count来存储接口数量
顺序是类索引,父类索引与接口索引数量,接口索引

第四部分:描述字段的信息

字段表(fields_count和filed)

字段表(field_info)用于描述接口或类中声明的变量。字段包括了类级变量或实例级变量,但不包括在方法内声明的变量字段的名字、数据类型、修饰符等都是无法固定的,只能引用常量池中的常量来描述。

字段表结构

fields_count()u2字段数目表示Class文件中的类变量和实例变量总数
这里写图片描述
字段修饰符放在access_flags项目中,它与类中的access_flags项目是非常类似的,都是一个u2的数据类型
这里写图片描述
跟随access_flags标志的是两项索引值:name_index和descriptor_index。它们都是对常量池的引用,分别代表着字段的简单名称(name_index)以及字段和方法的描述符(descriptor_index),这里简单介绍一下简单名称和描述符。

简单名称,描述符和全限定名

  • 全限定名:指一个事物的完整的名称,如在org.lxh.test包下的TestClass类的全限定名为:org/lxh/test/TestClass,即把包名中的“.”改为“/”,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“,”来表示全限定名结束

  • 简单名称:指没有类型或参数修饰的方法或字段名称,如果一个类中有这样一个方法boolean get(int name)和一个变量private final static int m,则他们的简单名称则分别为get()和m。

  • 描述符:是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序等)和返回值的。根据描述符规则,详细的描述符标示字的含义如下表所示:

这里写图片描述

字段表存储的常用内容

field字段包括了类级变量(静态变量)和实例级变量(成员变量),但不包括方法内部的局部变量。字段存放的信息包括:字段访问标志、是否静态、是否final、是否并发可见volatile、是否可序列化transient、数据类型、字段名称等等

其中的access_flags与类中的access_flagsfei类似,是表示数据类型的修饰符,如public、static、volatile等。后面的name_index和descriptor_index都是对常量池的引用,分别代表字段的简单名称及字段和方法的描述符

这里写图片描述

用方法描述符描述方法时,按照先参数后返回值的顺序描述,参数要按照严格的顺序放在一组小括号内,如方法 int getIndex(String name,char[] tgc,int start,int end,char target)的描述符为“(Ljava/lang/String[CIIC)I”。

字段表包含的固定数据项目到descriptor_index为止就结束了,但是在它之后还紧跟着一个属性表集合用于存储一些额外的信息。比如,如果在类中有如下字段的声明:static final int m = 2;那就可能会存在一项名为ConstantValue的属性,它指向常量2。关于attribute_info的详细内容,在后面关于属性表的项目中会有详细介绍。

最后需要注意一点:字段表集合中不会列出从父类或接口中继承而来的字段,但有可能列出原本Java代码中不存在的字段。比如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

部分分隔符================TML

第五部分:描述方法的信息

方法表(methods_count和methods)

methods(method_info)方法表用于描述类或者接口中声明的方法 methods_count(u2)用于表示Class文件中方法总数

方法表结构

这里写图片描述

方法表中存储的主要信息

method方法存储了方法的访问标识、是否静态、是否final、是否同步synchronized、是否本地方法native、是否抽象方法abstract、方法返回值类型、方法名称、方法参数列表等信息。
方法访问标志:
这里写图片描述
方法的代码指令并没有直接存放在方法表中,而是存放着属性表中的方法表Code中

注意:如果父类的方法在子类没有被重写,方法表中不会出现来自父类的方法信息,但是编译器会自动添加类构造器””方法和实例构造器””方法。

在Java语言中,要重载一个方法,除了要与原方法具有相同的简单名称外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名之中,因此Java语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。

部分分隔符================TML

第六部分:描述专有信息

属性表(attributes_count和attributes)

attributes(attribute_info)属性表是Class文件格式中最具扩展性的一种数据项目,用于存放field_info字段表、method_info方法表以及Class文件的专有信息,属性表不要求各个属性有严格顺序,只要求不与已有的属性名字重复即可,attributes_count(u2)表示属性表个数。

属性表集合的限制没有那么严格,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,但Java虚拟机运行时会忽略掉它不认识的属性。Java虚拟机规范中预定义了9项虚拟机应当能识别的属性(JDK1.5后又增加了一些新的特性,因此不止下面9项,但下面9项是最基本也是必要,出现频率最高的),如下表所示:
这里写图片描述

属性表结构

这里写图片描述

对于每个属性,它的名称都需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,每个属性值的结构是完全可以自定义的,只需说明属性值所占用的位数长度即可。一个符合规则的属性表至少有“attribute_name_info”、“attribute_length”和至少一项信息属性。

作用于类文件

InnerClasses属性

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

SourceFile属性

它用于记录生成这个Class文件的源码文件名称。

作用于类,方法表,字段表

Deprecated属性

该属性用于表示某个类、字段和方法,已经被程序作者定为不再推荐使用,它可以通过在代码中使用@Deprecated注释进行设置。

Synthetic属性

该属性代表此字段或方法并不是Java源代码直接生成的,而是由编译器自行添加的,如this字段和实例构造器、类构造器等

作用于方法表

Code属性表

前面已经说过,Java程序方法体中的代码讲过Javac编译后,生成的字节码指令便会存储在Code属性中,但并非所有的方法表都必须存在这个属性,比如接口或抽象类中的方法就不存在Code属性。如果方法表有Code属性存在,那么它的结构将如下表所示:
这里写图片描述

attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为“Code”,它代表了该属性的名称。attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共是6个字节,所以属性值的长度固定为整个属性表的长度减去6个字节。

max_stack代表了操作数栈深度的最大值,max_locals代表了局部变量表所需的存储空间,它的单位是Slot,并不是在方法中用到了多少个局部变量,就把这些局部变量所占Slot之和作为max_locals的值,原因是局部变量表中的Slot可以重用。

code_length和code用来存储Java源程序编译后生成的字节码指令。code用于存储字节码指令的一系列字节流,它是u1类型的单字节,因此取值范围为0x00到0xFF,那么一共可以表达256条指令,目前,Java虚拟机规范已经定义了其中200条编码值对应的指令含义。code_length虽然是一个u4类型的长度值,理论上可以达到2^32-1,但是虚拟机规范中限制了一个方法不允许超过65535条字节码指令,如果超过了这个限制,Javac编译器将会拒绝编译。

字节码指令之后是这个方法的显式异常处理表集合(exception_table),它对于Code属性来说并不是必须存在的。它的格式如下表所示:

这里写图片描述
它包含四个字段,这些字段的含义为:如果字节码从第start_pc行到第end_pc行之间(不含end_pc行)出现了类型为catch_type或其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引),则转到第handler_pc行继续处理,当catch_pc的值为0时,代表任何的异常情况都要转到handler_pc处进行处理

异常表实际上是Java代码的一部分,编译器使用异常表而不是简单的跳转命令来实现Java异常即finally处理机制,也因此,finally中的内容会在try或catch中的return语句之前执行,并且在try或catch跳转到finally之前,会将其内部需要返回的变量的值复制一份副本到最后一个本地表量表的Slot中

作用于Code属性表

LineNumberTable属性:它用于描述Java源码行号与字节码行号之间的对应关系。
LocalVariableTable属性: 它用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的对应关系。

Exception属性

这里的Exception属性的作用是列举出方法中可能抛出的受查异常,也就是方法描述时在throws关键字后面列举的异常。结构只有attribute_name_index、attribute_length、number_of_exceptions、exception_index_table四项

作用于字段表

ConstantValue属性

ConstantValue属性的作用是通知虚拟机自动为静态变量赋值,只有被static修饰的变量才可以使用这项属性。在Java中,对非static类型的变量(也就是实例变量)的赋值是在实例构造器方法中进行的;而对于类变量(static变量),则有两种方式可以选择:在类构造其中赋值,或使用ConstantValue属性赋值

目前Sun Javac编译器的选择是:如果同时使用final和static修饰一个变量(即全局常量),并且这个变量的数据类型是基本类型或String的话,就生成ConstantValue属性来进行初始化(编译时Javac将会为该常量生成ConstantValue属性,在类加载的准备阶段虚拟机便会根据ConstantValue为常量设置相应的值),如果该变量没有被final修饰,或者并非基本类型及字符串,则选择在方法中进行初始化

虽然有final关键字才更符合”ConstantValue“的含义,但在虚拟机规范中并没有强制要求字段必须用final修饰,只要求了字段必须用static修饰,对final关键字的要求是Javac编译器自己加入的限制。因此,在实际的程序中,只有同时被final和static修饰的字段才有ConstantValue属性。而且ConstantValue的属性值只限于基本类型和String,很明显这是因为它从常量池中也只能够引用到基本类型和String类型的字面量。

下面简要说明下final、static、static final修饰的字段赋值的区别:

  • static修饰的字段在类加载过程中的准备阶段被初始化为0或null等默认值,而后在初始化阶段(触发类构造器)才会被赋予代码中设定的值,如果没有设定值,那么它的值就为默认值。
  • final修饰的字段在运行时被初始化(可以直接赋值,也可以在实例构造器中赋值),一旦赋值便不可更改
  • static final修饰的字段在Javac时生成ConstantValue属性,在类加载的准备阶段根据ConstantValue的值为该字段赋值,它没有默认值,必须显式地赋值,否则Javac时会报错。可以理解为在编译期即把结果放入了常量池中

范例

public class Test {        public int getNum(int i) {            return i + 1;        }    }  

javap反编译之后

ublic class Test extends java.lang.Object      SourceFile: "Test.java"      minor version: 0      major version: 50    //常量池   Constant pool:    const #1 = class        #2;       const #2 = Asciz        Test;    const #3 = class        #4;       const #4 = Asciz        java/lang/Object;    const #5 = Asciz        <init>;  //实例构造器  const #6 = Asciz        ()V;  //void返回类型  const #7 = Asciz        Code;  //属性表Code属性  const #8 = Method       #3.#9;  //方法特征签名  java/lang/Object."<init>":()V    const #9 = NameAndType  #5:#6;//  方法名称和返回值"<init>":()V    const #10 = Asciz       LineNumberTable;  //属性表源码行号和字节码指令对应表  const #11 = Asciz       LocalVariableTable;  //属性表方法局部变量表  const #12 = Asciz       this;  //Test类实例对象本身  const #13 = Asciz       LTest;;  //对象类型,Test类  const #14 = Asciz       getNum;  //方法名称  const #15 = Asciz       (I)I;  //方法参数列表为一个int类型和返回值为int类型  const #16 = Asciz       i;  //参数名称i  const #17 = Asciz       I;  //参数类型int  const #18 = Asciz       SourceFile;    const #19 = Asciz       Test.java;     //方法表  {    //构造函数(默认构造方法)  public Test();      Code:  //属性表Code属性     Stack=1, Locals=1, Args_size=1       0:   aload_0       1:   invokespecial   #8; //Method java/lang/Object."<init>":()V       4:   return      LineNumberTable:       line 2: 0      LocalVariableTable:  //属性表方法局部变量表     Start  Length  Slot  Name   Signature       0      5      0    this       LTest;    //自定义方法  public int getNum(int);      Code:       Stack=2, Locals=2, Args_size=2       0:   iload_1       1:   iconst_1       2:   iadd       3:   ireturn      LineNumberTable:       line 4: 0      LocalVariableTable:       Start  Length  Slot  Name   Signature       0      4      0    this       LTest;       0      4      1    i       I    }    
阅读全文
0 0