精华内容
参与话题
问答
  • 类文件结构

    万次阅读 2020-04-16 10:39:12
    类文件结构 JVM 的“无关性” 谈论 JVM 的无关性,主要有以下两个: 平台无关性:任何操作系统都能运行 Java 代码 语言无关性: JVM 能运行除 Java 以外的其他代码 Java 源代码首先需要使用 Javac 编译器编译成 ....

    类文件结构

    JVM 的“无关性”

    谈论 JVM 的无关性,主要有以下两个:

    • 平台无关性:任何操作系统都能运行 Java 代码
    • 语言无关性: JVM 能运行除 Java 以外的其他代码
      Java 源代码首先需要使用 Javac 编译器编译成 .class 文件,然后由 JVM 执行 .class 文件,从而程序开始运行。

    JVM 只认识 .class 文件,它不关心是何种语言生成了 .class 文件,只要 .class 文件符合 JVM 的规范就能运行。 目前已经有 JRuby、Jython、Scala 等语言能够在 JVM 上运行。它们有各自的语法规则,不过它们的编译器 都能将各自的源码编译成符合 JVM 规范的 .class 文件,从而能够借助 JVM 运行它们。

    Java 语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成的,
    因此字节码命令所能提供的语义描述能力肯定会比 Java 语言本身更加强大。 因此,
    有一些 Java 语言本身无法有效支持的语言特性,不代表字节码本身无法有效支持。
    

    Class 文件结构

    Class 文件是二进制文件,它的内容具有严格的规范,文件中没有任何空格,全都是连续的 0/1。Class 文件 中的所有内容被分为两种类型:无符号数、表。

    • 无符号数 无符号数表示 Class 文件中的值,这些值没有任何类型,但有不同的长度。u1、u2、u4、u8 分别代表 1/2/4/8 字节的无符号数。
    • 表 由多个无符号数或者其他表作为数据项构成的符合数据类型。

    查看class文件的两种方法

    1、通过jdk自带的javap方法查看经过计算的class文件:

    命令:javap -verbose XXX.class
    class info
    2、通过16进制编辑器winhex。

    直接将生成的class文件丢进工具。

    这种方式。可以清楚的看到每个字节的内容,比如前四个字节的魔术,接着4个字节的版本信息,然后是常量池部分。
    class
    Class 文件具体由以下几个构成:

    • 魔数
    • 版本信息
    • 常量池
    • 访问标志
    • 类索引、父类索引、接口索引集合
    • 字段表集合
    • 方法表集合
    • 属性表集合

    魔数

    Class 文件的头 4 个字节称为魔数,用来表示这个 Class 文件的类型。

    Class 文件的魔数是用 16 进制表示的“CAFE BABE”,是不是很具有浪漫色彩?
    魔数相当于文件后缀名,只不过后缀名容易被修改,不安全,因此在 Class 文件中标识文件类型比较合适。

    版本信息

    紧接着魔数的 4 个字节是版本信息,5-6 字节表示次版本号,7-8 字节表示主版本号,它们表示当前 Class 文件中使用的是哪个版本的 JDK。

    高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式并未发生任何变化,虚拟机也必需拒绝执行超过其版本号的 Class 文件。

    常量池

    版本信息之后就是常量池,常量池中存放两种类型的常量:

    • 字面值常量

    字面值常量就是我们在程序中定义的字符串、被 final 修饰的值。

    • 符号引用

    符号引用就是我们定义的各种名字:类和接口的全限定名、字段的名字和描述符、方法的名字和描述符。

    常量池的特点

    • 常量池中常量数量不固定,因此常量池开头放置一个 u2 类型的无符号数,用来存储当前常量池的容量。
    • 常量池的每一项常量都是一个表,表开始的第一位是一个 u1 类型的标志位(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 字段或方法的符号引用
    CONSTANT_MethodHandle_info 15 表示方法句柄
    CONSTANT_MethodType_info 16 标识方法类型
    CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点

    对于 CONSTANT_Class_info(此类型的常量代表一个类或者接口的符号引用),它的二维表结构如下:

    类型 名称 数量
    u1 tag 1
    u2 name_index 1

    tag 是标志位,用于区分常量类型;name_index 是一个索引值,它指向常量池中一个 CONSTANT_Utf8_info 类型常量,此常量代表这个类(或接口)的全限定名,这里 name_index 值若为 0x0002,也即是指向了常量池中的第二项常量。

    CONSTANT_Utf8_info 型常量的结构如下:

    类型 名称 数量
    u1 tag 1
    u2 length 1
    u1 bytes length

    tag 是当前常量的类型;length 表示这个字符串的长度;bytes 是这个字符串的内容(采用缩略的 UTF8 编码)

    访问标志

    在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为 public 类型;是否被 abstract/final 修饰。

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

    类索引和父类索引都是一个 u2 类型的数据,而接口索引集合是一组 u2 类型的数据的集合,Class 文件中由这三项数据来确定类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。

    由于 Java 不允许多重继承,所以父类索引只有一个,除了 java.lang.Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0。一个类可能实现了多个接口,因此用接口索引集合来描述。这个集合第一项为 u2 类型的数据,表示索引表的容量,接下来就是接口的名字索引。

    类索引和父类索引用两个 u2 类型的索引值表示,它们各自指向一个类型为 CONSTANT_Class_info 的类描述符常量,通过该常量总的索引值可以找到定义在 CONSTANT_Utf8_info 类型的常量中的全限定名字符串。

    字段表集合

    字段表集合存储本类涉及到的成员变量,包括实例变量和类变量,但不包括方法中的局部变量。

    每一个字段表只表示一个成员变量,本类中的所有成员变量构成了字段表集合。字段表结构如下:

    类型 名称 数量 说明
    u2 access_flags 1 字段的访问标志,与类稍有不同
    u2 name_index 1 字段名字的索引
    u2 descriptor_index 1 描述符,用于描述字段的数据类型。 基本数据类型用大写字母表示; 对象类型用“L 对象类型的全限定名”表示。
    u2 attributes_count 1 属性表集合的长度
    u2 attributes attributes_count 属性表集合,用于存放属性的额外信息,如属性的值。

    字段表集合中不会出现从父类(或接口)中继承而来的字段,但有可能出现原本 Java 代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

    方法表集合

    方法表结构与属性表类似。

    volatile 关键字 和 transient 关键字不能修饰方法,所以方法表的访问标志中没有 ACC_VOLATILE 和 ACC_TRANSIENT 标志。

    方法表的属性表集合中有一张 Code 属性表,用于存储当前方法经编译器编译后的字节码指令。

    属性表集合

    每个属性对应一张属性表,属性表的结构如下:

    类型 名称 数量
    u2 attribute_name_index 1
    u4 attribute_length 1
    u1 info attribute_length
    展开全文
  • 深入理解Java虚拟机--类文件结构

    万次阅读 2017-05-18 00:20:40
    Class类文件结构 魔数与Class文件的版本 常量池 访问标志 类索引、 父类索引与接口索引集合 字段表集合 方法表集合 属性表集合 Code属性 Exceptions属性 LineNumberTable属性 LocalVariableTable属性 ...

    目录

    Class类文件的结构

    魔数与Class文件的版本

    常量池

    访问标志

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

    字段表集合

    方法表集合

    属性表集合

    Code属性

    Exceptions属性

    LineNumberTable属性

    LocalVariableTable属性

    ConstantValue属性


    参考书籍:《深入理解Java虚拟机——JVM高级特性与最佳实践(第2版)

    Java在刚刚诞生之时曾经提出过一个非常著名的宣传口号:“一次编写,到处运行(Write Once,Run Anywhere)”,这句话充分表达了软件开发人员对冲破平台界限的渴求。而实现这个“与平台无关”理想的基础就是虚拟机和字节码(ByteCode)格式存储。 

    Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class 文件中包含了 Java 虚拟机指令集(或者称为字节码、 Bytecodes)和符号表,还有一些其他辅助信息。使用不同语言所编写的代码只要能够被正确编译为符合虚拟机规范要求的Class文件,虚拟机就能够执行它,虚拟机并不关心Class文件的来源是何种语言。  

    Class类文件的结构

    Class文件格式所具备的平台中立(不依赖于特定硬件及操作系统)、 紧凑、 稳定和可扩展的特点,是Java技术体系实现平台无关、 语言无关两项特性的重要支柱。

    Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。 当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。

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

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

    表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾,用于描述有层次关系的复合结构的数据。

    整个Class文件本质上就是一张表,它由下表所示的数据项构成。

    样例源码:  

    package jvm;
    
    public class SimpleClass implements Comparable<SimpleClass> {
    
    	private static final int magic = 0xCAFEBABE;
    
    	private int number;
    
    	public void setNumber(int number) {
    		this.number = number;
    	}
    
    	public int compareTo(SimpleClass o) {
    		if (this.number == o.number) {
    			return 0;
    		}
    		int ret = this.number > o.number ? 1 : -1;
    		return ret;
    	}
    
    }

    将以上代码使用JDK 1.6编译输出的Class文件在UltraEdit中打开,其前64个字节的内容如下图: 

    魔数与Class文件的版本

    每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。 很多文件存储标准中都使用魔数来进行身份识别,譬如图片格式,如gif或者jpeg等在文件头中都存有魔数。 使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。 如上图所示,Class文件的魔数值为:0xCAFEBABE

    紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version,图中为0x0000),第7和第8个字节是主版本号(Major Version,图中为0x0031)。

    常量池

    由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。 与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的,如上图所示,常量池容量(偏移地址:0x00000008)为十六进制数0x0024,即十进制的36,这就代表常量池中有35项常量,索引值范围为1~35。 在Class文件格式规范制定之时,设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以把索引值置为0来表示。Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、 字段表集合、 方法表集合等的容量计数都与一般习惯相同,是从0开始的。

    常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、 声明为final的常量值等。 而符号引用则属于编译原理方面的概念,包括了下面三类常量:

    • 类和接口的全限定名(Fully Qualified Name)
    • 字段的名称和描述符(Descriptor)
    • 方法的名称和描述符

    常量池中每一项常量都是一个表,从JDK 1.7开始,共分为以下14种表类型。

    类  型 标  志 描  述
    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_InvokeDynamic_info 18 标识一个动态方法调用点

    UltraEdit截图中常量池的第一项常量的标志位(偏移地址:0x0000000a)是0x07,表示这个常量属于CONSTANT_Class_info类型,此类型的常量代表一个类或者接口的符号引用。 CONSTANT_Class_info的结构比较简单,见下表。

    类  型 名  称 数  量
    u1 tag 1
    u2 name_index 1

    tag是标志位,用于区分常量类型;name_index是一个索引值,它指向常量池中一个CONSTANT_Utf8_info类型常量,此常量代表了这个类(或者接口)的全限定名,这里name_index值(偏移地址:0x0000000b)为0x0002,也即是指向了常量池中的第二项常量。 继续从UltraEdit截图中查找第二项常量,它的标志位(地址:0x0000000d)是0x01,表示这个常量属于CONSTANT_Utf8_info类型,CONSTANT_Utf8_info类型的结构见下表。

    类  型 名  称 数  量
    u1 tag 1
    u2 length 1
    u1 bytes length

    length值说明了这个UTF-8编码的字符串长度是多少字节,它后面紧跟着的长度为length字节的连续数据是一个使用UTF-8缩略编码表示的字符串。 UTF-8缩略编码与普通UTF-8编码的区别是:

    • 从'\u0001'到'\u007f'之间的字符(相当于1~127的ASCII码)的缩略编码使用一个字节表示;
    • 从'\u0080'到'\u07ff'之间的所有字符的缩略编码用两个字节表示;
    • 从'\u0800'到'\uffff'之间的所有字符的缩略编码就按照普通UTF-8编码规则使用三个字节表示。

    本例中这个字符串的length值(偏移地址:0x0000000e)为0x000F,也就是长15字节,往后15字节正好都在1~127的ASCII码范围以内,内容为“jvm/SimpleClass”,换算结果如下图选中的部分所示。

    其余的34个常量,我们可以通过类似的方法计算出来,或使用javap工具帮助我们进行计算,以下是使用javap工具的-verbose参数输出的SimpleClass.class的常量池内容。

    常量池中14种常量项的结构总表。

    访问标志

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

    标志名称 标志量 含  义
    ACC_PUBLIC 0x0001 是否为public类型
    ACC_FINAL 0x0010 是否被声明为final,只有类可设置
    ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语意,invokespecial指令的语意在JDK 1.0.2发生过改变,为了区别这条指令使用哪种语意,JDK 1.0.2之后编译出来的类的这个标志都必须为真
    ACC_INTERFACE 0x0200 标识这是一个接口
    ACC_ABSTRACT 0x4000 是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其它类值为假
    ACC_SYNTHETIC 0x1000 标识这个类并非由用户代码产生的
    ACC_ANNOTATION 0x2000 标识这是一个注解
    ACC_ENUM 0x4000 标识这是一个枚举

    access_flags中一共有16个标志位可以使用,当前只定义了其中8个,没有使用到的标志位要求一律为0。UltraEdit截图中的access_flags标志(偏移地址:0x000001b2)值为0x0021,代表SimpleClass是一个public访问权限的普通java类。

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

    类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。 类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。 由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。 接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。

    类索引、 父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。

     SimpleClass类仅实现了一个Comparable接口,其接口计数器偏移地址:0x00001b07,其后紧跟0x0005指向一个CONSTANT_Class_info类型的索引值,从常量#6可以看出,这个索引指向的就是Comparable接口。

    字段表集合

    字段表(field_info)用于描述接口或者类中声明的变量。 字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。

    字段表结构如下表。

    类  型 名  称 数  量
    u2 access_flag 1
    u2 name_index 1
    u2 descriptor_index 1
    u2 attributes_count 1
    attribute_info attributes attributes_count

    其中access_flag可以设置的访问标志位和含义见下表。

    标志名称 标志量 含  义
    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_TRANSIENT 0x0080 字段是否为transient
    ACC_SYNTHETIC 0x1000 字段是否由编译器自动产生的
    ACC_ENUM 0x4000 字段是否为enum

    跟随access_flags标志的是两项索引值:name_index和descriptor_index。 它们都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符。

    所谓全限定,仅仅是把类全名中的“.”替换成了“/”而已,例如:“java/lang/Comparable”,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束。 简单名称是指没有类型和参数修饰的方法或者字段名称,例如,SimpleClass类中的setNumber()方法和magic字段的简单名称分别是“setNumber”和“magic”。

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

    标识字符

    含义

    标识字符

    含义

    B

    基本类型byte

    J

    基本类型long

    C

    基本类型char

    S

    基本类型short

    D

    基本类型double

    Z

    基本类型boolean

    F

    基本类型float

    V

    特殊类型void

    I

    基本类型int

    L

    对象类型,如:Ljava/lang/Object

    对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[][]”类型的二维数组,将被记录为:“[[Ljava/lang/String;”,一个整型数组“int[]”将被记录为“[I”。

    用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。 如方法void inc()的描述符为“()V”,方法java.lang.String toString()的描述符为“()Ljava/lang/String;”,方法intindexOf(char[]source,int sourceOffset,int sourceCount,char[] target,int targetOffset,int targetCount,int fromIndex)的描述符为“([CII[CIII)I”。

    对于SimpleClass.class文件来说,字段表集合从地址0x000001bb开始,第一个u2类型的数据为容量计数器fields_count,如图所示,其值为0x0002,说明这个类有两个字段表数据。 接下来紧跟着容量计数器的是第一个字段的access_flags标志,值为0x001A,代表该字段被private+static+final修饰。 代表字段名称的name_index的值为0x0007,从常量池中可查得第7项常量名为“magic”,代表字段描述符的descriptor_index的值为0x0008,指向常量池的字符串“I”,即该字段为int类型。

    字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

    方法表集合

    Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,方法表的结构如同字段表一样,依次包括了访问标志(access_flags)、 名称索引(name_index)、 描述符索引(descriptor_index)、 属性表集合(attributes)几项,见下表。

    类  型 名  称 数  量
    u2 access_flag 1
    u2 name_index 1
    u2 descriptor_index 1
    u2 attributes_count 1
    attribute_info attributes attributes_count

    对于方法表,所有标志位及其取值可参见下表。

    标志名称 标志量 含  义
    ACC_PUBLIC 0x0001 方法是否为public
    ACC_PRIVATE 0x0002 方法是否为private
    ACC_PROTECTED 0x0004 方法是否为protected
    ACC_STATIC 0x0008 方法是否为static
    ACC_FINAL 0x0010 方法是否为final
    ACC_SYNCHRONIZED 0x0020 方法是否为synchronized
    ACC_BRIDGE 0x0040 方法是否有编译器产生的桥接方法
    ACC_VARARGS 0x0080 方法是否接受不定参数
    ACC_NATIVE 0x0100 方法是否为native
    ACC_ABSTRACT 0x0400 方法是否为abstract
    ACC_STRICTFP 0x0800 方法是否为strictfp
    ACC_SYNTHETIC 0x1000 方法是否是由编译器自动产生的

    对于SimpleClass.class文件来说,方法表集合的入口地址为:0x00001d04,第一个u2类型的数据(即是计数器容量)的值为0x0004,代表集合中有四个方法(这四个方法为编译器添加的实例构造器<init>、Comparable接口的构造方法以及源码中的方法compareTo()和setNumber()方法)。 第一个方法的访问标志值为0x001,也就是只有ACC_PUBLIC标志为真,名称索引值为0x000C,从常量池中可查得第12项常量池的方法名为“<init>”,描述符索引值为0x000D,对应常量为“()V”,属性表计数器attributes_count的值为0x0001就表示此方法的属性表集合有一项属性,属性名称索引为0x000E,对应常量为“Code”,说明此属性是方法的字节码描述。

    与字段表集合相对应的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。 但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“<clinit>”方法和实例构造器“<init>”方法。

    属性表集合

    属性表(attribute_info)在前面的讲解之中已经出现过数次,在Class文件、 字段表、 方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。

    属性表的预定义属性目前共有21项,例如:Code、ConstantValue、LocalVariableTable、LineNumberTable。

    Code属性

    Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在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型常量的索引,常量值固定为“Code”,它代表了该属性的属性名称,attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共为6字节,所以属性值的长度固定为整个属性表长度减去6个字节。

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

    max_locals代表了局部变量表所需的存储空间。 在这里,max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。 对于byte、 char、 float、 int、 short、 boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double和long这两种64位的数据类型则需要两个Slot来存放。 方法参数(包括实例方法中的隐藏参数“this”)、 显式异常处理器的参数(Exception Handler Parameter,就是try-catch语句中catch块所定义的异常)、 方法体中定义的局部变量都需要使用局部变量表来存放。 另外,并不是在方法中用到了多少个局部变量,就把这些局部变量所占Slot之和作为max_locals的值,原因是局部变量表中的Slot可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量所占的Slot可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配Slot给各个变量使用,然后计算出max_locals的大小。

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

    其中异常表的结构如下表。

    类  型 名  称 数  量
    u2 start_pc 1
    u2 end_pc 1
    u2 handler_pc 1
    u2 catch_type 1

    Exceptions属性

    Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Excepitons),也就是方法描述时在throws关键字后面列举的异常。 它的结构见下表。

    类  型 名  称 数  量
    u2 attribute_name_index 1
    u4 attribute_length 1
    u2 number_of_exceptions 1
    u2 exception_index_table number_of_exceptions

    LineNumberTable属性

    LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。 它的结构见下表。

    类  型 名  称 数  量
    u2 attribute_name_index 1
    u4 attribute_length 1
    u2 line_number_table_length 1
    line_number_info line_number_table line_number_table_length

    LocalVariableTable属性

    LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。 它的结构见下表。

    类  型 名  称 数  量
    u2 attribute_name_index 1
    u4 attribute_length 1
    u2 local_variable_table_length 1
    local_variable_info local_variable_table local_variable_table_length

    其中,local_variable_info项目代表了一个栈帧与源码中的局部变量的关联,结构下表。

    类  型 名  称 数  量
    u2 start_pc 1
    u2 length 1
    u2 name_index 1
    u2 descriptor_index 1
    u2 index 1

    ConstantValue属性

    ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。 只有被static关键字修饰的变量(类变量)才可以使用这项属性。

    类  型 名  称 数  量
    u2 attribute_name_index 1
    u4 attribute_length 1
    u2 constantvalue_index 1
    展开全文
  • Class类文件结构

    千次阅读 2016-05-29 16:31:34
    接下来,就应该看看Java的加载机制,看看虚拟机是如何将Java代码文件编译后的class文件加载到Java内存中的。 Java是一门平台无关语言,只要有Java的运行环境,编写的代码可以运行在各种机器上,做到了“一次编码、...

    在了解了Java内存的分布、HotSpot虚拟机对Java对象的管理以及Java垃圾收集机制之后,我们大致了解了Java自动内存管理的部分。接下来,就应该看看Java的类加载机制,看看虚拟机是如何将Java代码文件编译后的class文件加载到Java内存中的。

    Java是一门平台无关语言,只要有Java的运行环境,编写的代码可以运行在各种机器上,做到了“一次编码、处处运行”的目的。为了达到平台无关,Sun公司以及其它虚拟机提供商发布了许多可以运行不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,达到了平台无关的目的,如下图:


    不过,java虚拟机不仅可以运行Java程序,在设计之初就实现了让其他语言运行在Java虚拟机上的可能性,只要程序编译之后能生成符合虚拟机规范的class文件即可。现在,已经有很多语言可以运行在java虚拟机上了,比如Clojure、Groovy、JRuby、Jython、Scala等。这样,java虚拟机也实现了语言无关性。上面的图就变成了这个样子:


    可以看出,语言无关性和平台无关性的关键在于class字节码文件。Java虚拟机规范要求class文件中使用许多强制性以及若干其他辅助信息。接下来就详细了解一下class字节码文件的结构,当然,这里主要以Java语言为主。

    1、概述

    class文件是一组以8位字节为基础的二进制流,各个数据项目严格按照顺序紧凑地排列在class文件中,中间没有任何分隔符,这点和png、jpg等图片文件格式类似。当遇到需要占用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

    无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会用到一个前置的容量计数器(表中以“_count”结尾的项)加上若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。

    为了介绍各个数据项的含义,这里以一个简单的Java程序为例,代码如下:

    public class Test{
    	public static double PI=3.14;
    	private int m;
    	public int inc(){
    		return m+1;
    	}
    }

    在命令行下使用javac编译就可以得到class文件,如下:


    接下来看看各项的具体含义。

    2、魔数与Class文件的版本

    这部分的二进制流内容:


    class文件的头4个字节称为魔数,它的唯一作用就是确定这个文件时候是一个能被虚拟机接受的class文件。很多图片格式都用一个魔数来标识文件类型,比如png和jpg等。在java的class文件中,这个数是0xcafebabe。

    接下来就是class文件的版本号,第5、6个字节是次版本号,第7、8个字节是主版本号。在这里,次版本号是0,主版本号是52,(十六进制是34)。Java的版本号是从45开始的,JDK1.1之后的每一个JDK大版本发布主版本号向上加1,高版本的JDK能向下兼容低版本的JDK。

    3、常量池

    紧接着主版本号的就是常量池,常量池可以理解为class文件的资源仓库,它是class文件结构中与其它项目关联最多的数据类型,也是占用class文件空间最大的数据项目之一,也是class文件中第一个出现的表类型数据项目。

    由于常量池中常量的数量不是固定的,所以常量池入口需要放置一项u2类型的数据,代表常量池中的容量计数。不过,这里需要注意的是,这个容器计数是从1开始的而不是从0开始,也就是说,常量池中常量的个数是这个容器计数-1。将0空出来的目的是满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义。class文件中只有常量池的容量计数是从1开始的,对于其它集合类型,比如接口索引集合、字段表集合、方法表集合等的容量计数都是从0开始的。

    常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近Java语言的常量概念,如文本字符串、声明为final的常量等。而符号引用则属于编译原理方面的概念,它包括三方面的内容:

    • 类和接口的全限定名(Fully Qualified Name);
    • 字段的名称和描述符(Descriptor);
    • 方法的名称和描述符;

    Java代码在进行javac编译的时候并不像C和C++那样有连接这一步,而是在虚拟机加载class文件的时候进行动态连接。也就是说,在class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,虚拟机也就无法使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址中。

    常量池中的每一项都是一个表,在JDK1.7之前有11中结构不同的表结构,在JDK1.7中为了更好的支持动态语言调用,又增加了3种(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info)。不过这里不会介绍这三种表数据结构。

    这14个表的开始第一个字节是一个u1类型的tag,用来标识是哪一种常量类型。这14种常量类型所代表的含义如下:

    类型

    标志

    含义

    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_InterfaceMethod_info

    11

    接口中方法的符号引用

    CONSTANT_NameAndType_info

    12

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

    CONSTANT_MethodHandle_info

    15

    表示方法句柄

    CONSTANT_MethodType_info

    16

    标识方法类型

    CONSTANT_InvokeDynamic_info

    18

    表示一个动态方法调用点

    本例的常量池部分如下:


    蓝颜色覆盖的是常量池部分,可以看到这部分的内容非常多。因为常量池中的常量比较多,每一中常量还有自己的结构,导致常量池的结构非常复杂,这里也仅仅是简单解析两个例子。

    由class文件结构图可知,常量池的开头两个字节0x001A是常量池的容量计数,这里是26,也就是说,这个常量池中有25个常量项。看看这个例子的第一项,容量计数后面的第一个字节标识这个常量的类型,是0A,即10,查表可知是类方法的符号引用,这个常量表的结构如下:

    类型

    名称

    数量

    U1

    tag

    1

    U2

    name_index

    1

    U2

    descriptor_index

    1

    按照这个结构,可以知道name_index是7(0x0007),descriptor_index是21(0x0015)。这都是一个索引,指向常量池中的其他常量,其中name描述了这个方法的名称,descriptor描述了这个方法的访问标志(比如public、private等)、参数类型和返回类型。

    接下来的tag是9,可知是一个字段的符号引用,它的结构和方法的结构类似,只不过接下来的两个字节表示的是声明这个字段的类或接口的索引,最后的两个字节表示的是这个字段的类型和名字CONSTANT_NameAndType索引,这两个索引分别是6和22,在后面会验证这几个索引。

    根据这两个例子可以看出,要准确的描述一个类中所声明的字段和方法的所有信息,仅仅一个符号引用是不够的,还需要继续引用其他的常量池项目。

    常量池中接下来的内容也可以这样解析,不过,JDK已经提供了一个工具可以自动计算这些内容,使用javap -verbose命令可以快速的计算出class文件结构的内容,比如这样:

    javap -verbose Test

    注意Test没有.java 或.class,它是解析Test.class文件的,所以使用前先用javac编译Java文件。结果如下:

    Classfile /C:/Users/Liu Guoqiang/Desktop/Test.class
      Last modified 2016-5-29; size 357 bytes
      MD5 checksum cc9fcfb483f1dc499e7535bfe9f88943
      Compiled from "Test.java"
    public class Test
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #7.#21         // java/lang/Object."<init>":()V
       #2 = Fieldref           #6.#22         // Test.m:I
       #3 = Double             3.14d
       #5 = Fieldref           #6.#23         // Test.PI:D
       #6 = Class              #24            // Test
       #7 = Class              #25            // java/lang/Object
       #8 = Utf8               PI
       #9 = Utf8               D
      #10 = Utf8               m
      #11 = Utf8               I
      #12 = Utf8               <init>
      #13 = Utf8               ()V
      #14 = Utf8               Code
      #15 = Utf8               LineNumberTable
      #16 = Utf8               inc
      #17 = Utf8               ()I
      #18 = Utf8               <clinit>
      #19 = Utf8               SourceFile
      #20 = Utf8               Test.java
      #21 = NameAndType        #12:#13        // "<init>":()V
      #22 = NameAndType        #10:#11        // m:I
      #23 = NameAndType        #8:#9          // PI:D
      #24 = Utf8               Test
      #25 = Utf8               java/lang/Object
    {
      public static double PI;
        descriptor: D
        flags: ACC_PUBLIC, ACC_STATIC
    
      public Test();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 1: 0
    
      public int inc();
        descriptor: ()I
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=1, args_size=1
             0: aload_0
             1: getfield      #2                  // Field m:I
             4: iconst_1
             5: iadd
             6: ireturn
          LineNumberTable:
            line 5: 0
    
      static {};
        descriptor: ()V
        flags: ACC_STATIC
        Code:
          stack=2, locals=0, args_size=0
             0: ldc2_w        #3                  // double 3.14d
             3: putstatic     #5                  // Field PI:D
             6: return
          LineNumberTable:
            line 2: 0
    }
    SourceFile: "Test.java"

    可以看出,javap已经将class文件中所有的内容解析出来了,并且以一种友好的方式展示出来。根据这个内容,看看我们手动解析的结果。

    首先是一个方法符号引用,内容是7.21,查看结果,可以看到索引为7的常量是一个类符号引用,这个类符号引用的索引是25,然后看看索引是25的常量,是一个Utf8编码的字符串,内容是java/lang/Object。然后看看索引是21的常量,是一个NameAndType类型,这个常量的内容是12:13,索引是12的内容是<init>,索引是13的内容是()V,这表示了一个方法的名称、参数类型和返回类型,具体的含义在后面的方法表中介绍。这样,这个方法的内容就是java/lang/Object."<init>":()V。

    看起来这个<init>并没有在Java程序中出现,还有一些内容也没有在Java程序中出现,比如“I”、“V”、“LineNumberTable”等。这是自动生成的常量,但它们会被后面即将介绍到的字段表、方法表和属性表引用到,用来描述一些不方便使用固定字节表示的内容。

    最后,给出14种常量项的结构:

    常量

    项目

    类型

    含义

    CONSTANT_Utf8_info

    tag

    U1

    1

    length

    U2

    UTF-8编码的字符串的长度

    bytes

    U1

    长度为length的UTF-8编码的字符串

    CONSTANT_Integer_info

    Tag

    U1

    3

    bytes

    U4

    按照高位在前的int值

    CONSTANT_Float_info

    tag

    U1

    4

    bytes

    U4

    按照高位在前的float值

    CONSTANT_Long_info

    tag

    U1

    5

    bytes

    U8

    按照高位在前的long值

    CONSTANT_Double_info

    tag

    U1

    6

    bytes

    U8

    按照高位在前的double值

    CONSTANT_Class_info

    tag

    U1

    7

    index

    U2

    指向全限定名常量项的索引

    CONSTANT_String_info

    Tag

    U1

    8

    index

    U2

    指向字符串字面量的索引

    CONSTANT_Fieldref_info

    tag

    U1

    9

    index

    U2

    指向声明字段的类或接口描述符CONSTANT_Class_info的索引项

    index

    U2

    指向字段描述符CONSTANT_NameAndType_info的索引项

    CONSTANT_Methodref_info

    tag

    U1

    10

    index

    U2

    指向声明方法的类描述符CONSTANT_Class_info的索引项

    index

    U2

    指向名称及类描述符CONSTANT_NameAndType_info的索引项

    CONSTANT_InterfaceMethod_info

    tag

    U1

    11

    index

    U2

    指向声明方法的接口描述符COSNTANT_Class_info的索引项

    index

    U2

    指向名称及类描述符CONSTANT_NameAndType_info的索引项

    CONSTANT_NameAndType_info

    tag

    U1

    12

    index

    U2

    指向该字段或方法名称常量池的索引

    index

    U2

    指向该字段或方法描述符常量池的索引

    CONSTANT_MethodHandle_info

    tag

    U1

    15

    reference_kind

    U2

    值必须在1-9之间,决定了方法句柄的类型,方法句柄累心的值表示方法句柄的字节码行为

    reference_ index

    U2

    值必须是对常量池的有效索引

    CONSTANT_MethodType_info

    tag

    U1

    16

    descriptor_index

    U2

    值必须是对常量池的有效索引,常量池在改索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符

    CONSTANT_InvokeDynamic_info

    tag

    U1

    18

    bootstrap_method_attrindex

    U2

    值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引

    name_and_type_index

    U2

    值必须是对当前常量池的有效索引,常量池在该索引处的项必须是COSTANT_NameAndType_info结构,表示方法名和方法描述符


    4、访问标志

    常量池结束后紧接着的两个字节代表访问标志,用来标识一些类或接口的访问信息,包括:这个Class是类还是接口;是否定义为public;是否定义为abstract;如果是类的话,是否被声明为final等。具体的标志位以及含义如下表:

    标志名称

    标志值

    含义

    ACC_PUBLIC

    0x0001

    是否是public

    ACC_FINAL

    0x0010

    是否被声明为final,只有类可以设置

    ACC_SUPER

    0x0020

    是否允许使用invokespecial字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真

    ACC_INTERFACE

    0x0200

    标识是一个接口

    ACC_ABSTRACT

    0x0400

    是否是abstract,对于接口和抽象类来说为真,其他类都为假

    ACC_SYNITHETIC

    0x1000

    标识这个类并非由用户代码产生

    ACC_ANNOTATION

    0x2000

    标识这是一个注解

    ACC_ENUM

    0x4000

    标识这是一个枚举类

    这部分的二进制流内容如下:


    由于access_flags是两个字节大小,一共有十六个标志位可以使用,当前仅仅定义了8个,没有用到的标志位都是0。对于一个类来说,可能会有多个访问标志,这时就可以对照上表中的标志值取或运算的值。拿上面那个例子来说,它的访问标志值是0x0021,查表可知,这是ACC_PUBLIC和ACC_SUPER值取或运算的结果。所以Test这个类的访问标志就是ACC_PUBLIC和ACC_SUPER,这一点我们可以在javap得到的结果中验证。

    5、类索引、父类索引与接口索引集合

    在访问标志access_flags后接下来就是类索引(this_class)和父类索引(super_class),这两个数据都是u2类型的,而接下来的接口索引集合是一个u2类型的集合,class文件由这三个数据项来确定类的继承关系。由于Java中是单继承,所以父类索引只有一个;但Java类可以实现多个接口,所以接口索引是一个集合。

    类索引用来确定这个类的全限定名,这个全限定名就是说一个类的类名包含所有的包名,然后使用"/"代替"."。比如Object的全限定名是java.lang.Object。父类索引确定这个类的父类的全限定名,除了Object之外,所有的类都有父类,所以除了Object之外所有类的父类索引都不为0.接口索引集合存储了implements语句后面按照从左到右的顺序的接口。

    类索引和父类索引都是一个索引,这个索引指向常量池中的CONSTANT_Class_info类型的常量。然后再CONSTANT_Class_info常量中的索引就可以找到常量池中类型为CONSTANT_Utf8_info的常量,而这个常量保存着类的全限定名。

    这部分的二进制流内容如下:


    以上面的例子来说,this_class的值是0x0006,即十进制的6,指向的CONSTANT_Class_info中的索引是24,常量池中索引是24的CONSTANT_Utf8_info的常量是一个长度为4的字符串,值是“Test”。这样就解析到了这个类的全限定名,类的父类的全限定名也可以这样解析。下图是解析过程:


    由于这个类没有实现接口,所以接口索引集合的容量计数是0。如果容量计数是0,就不需要存储接口的信息。

    6、字段表集合

    字段表用来描述接口或类中声明的变量。字段包括类级变量和实例级变量,但不包括方法内变量。所谓的类级变量就是静态变量,这个变量不属于这个类的任何实例,可以不用定义类实例就可以使用;实例级变量不是静态变量,是和类实例相关联的,需要定义类实例才能使用。

    那么,声明一个变量需要哪些信息呢?有:字段的作用域(public、private和protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final修饰符)、并发可见性(volatile修饰符)、是否可被序列化(transient修饰符)、字段的数据类型(基本类型、对象、数组)以及字段名称。包含的信息有点多,不过不需要的可以不写。这些信息中,各个修饰符可以用布尔值表示。而字段叫什么名字、字段被定义为什么类型数据都是无法固定的,只能用常量池中的常量来表示。下面是字段表的格式:

    类型

    名称

    数量

    U2

    access_flags

    1

    U2

    name_index

    1

    U2

    descriptor_index

    1

    U2

    attributes_count

    1

    attribute_info

    attributes

    attributes_count

    其中的字段修饰符access_flags,和类中的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_TRANSIENT

    0x0080

    字段是否是transient

    ACC_SYNTHETIC

    0x1000

    字段是否是由编译器自动产生的

    ACC_ENUM

    0x4000

    字段是否是enum

    显然,ACC_PUBLIC、ACC_PRIVATE和ACC_PROTECTED只能选择一个,ACC_FINAL和ACC_VOLATILE不能同时选择。接口中的字段必须有ACC_PUBLIC、ACC_STATIC和ACC_FINAL标志,这是Java语言本身的规则决定的。

    access_flags给出了字段中所有可以用布尔值表示的修饰符,剩下的信息就是字段的名字、变量类型等信息。access_flags后面的是name_index和descriptor_index,前者是字段名的常量池索引,后者是字段描述符的常量池索引。name_index可以描述字段的名字,descriptor_index可以描述字段的数据类型。不过,对于方法的描述符来说就要复杂一些,因为一个方法除了返回值类型,还有参数类型,而且参数的个数还不确定。根据描述符规则,这些类型都使用一个大写字母来表示,如下表:

    标识字符

    含义

    标识字符

    含义

    B

    byte

    J

    long

    C

    char

    S

    short

    D

    double

    Z

    boolean

    F

    float

    V

    void

    I

    int

    L

    对象类型,如Ljava/lang/Object

    对于数组类型,每一个维度将使用一个前置的“[”字符来描述。比如定义一个java.lang.String[][]类型的二维数组,将记录为"[[Ljava/lang/String",一个double数组"double[]"将标记为"[D"。

    当描述符用来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号"()"内。比如方法void inc()的描述符是:()V。方法java.lang.String toString()的描述符是:()Ljava/lang/String。方法int indexOf(char[] source,int sourceOffset,int sourceCount,char[] target,int targetOffset,int targetCount,int fromIndex)的描述符是:([CII[CIII)I。

    descriptor_info后面是属性信息,这会在后面属性表集合中介绍。

    本部分的二进制流内容:


    以上面的例子为例,前两个字节代表字段的个数,这里是2个。接下来就是具体的字段信息。第一个字段表内容是:0009 0008 0009 0000,首先访问标志是9,可以看出是ACC_PUBLIC和ACC_STATIC,是一个静态常量,name_index是8,指向的常量项是CONSTANT_Utf8_info,内容是PI,描述符是8,常量池中的常量是CONSTANT_Utf8_info,内容是D,即double类型。所以这个常量是:public static double PI。和我们声明的一样,不过还有一点就是,我们声明的PI还有一个值:3.14,这个数在常量池中可以找到,索引是3的常量,不过这个值是如何与PI关联起来的,后面会介绍。

    同样的道理也能解析出第二个字段是:private int m。

    字段表集合中不会列出从父类或接口中继承来的字段,但有可能会出现原本Java程序中没有的字段。比较典型的例子是内部类,为了在内部类中保持对外部类的访问性,会增加一个指向外部类实例的字段。另外,在Java语言中字段无法重载,也就是字段名不能重复,即使两个字段的数据类型、修饰符都不相同。不过对于字节码来说,如果两个字段的描述符不一致,那么就可以有重复的字段名。

    7、方法表集合

    在字段表集合中介绍了字段的描述符和方法的描述符,对于理解方法表有很大帮助。class文件存储格式中对方法的描述和对字段的描述几乎相同,方法表的结构也和字段表相同,这里就不再列出。不过,方法表的访问标志和字段的不同,列出如下:

    标识名称

    标志值

    含义

    ACC_PUBLIC

    0x0001

    方法是否是public

    ACC_PRIVATE

    0x0002

    方法是否是private

    ACC_PUBLICPROTECTED

    0x0004

    方法是否是protected

    ACC_STATIC

    0x0008

    方法是否是static

    ACC_FINAL

    0x0010

    方法是否是final

    ACC_SYNCHRONIZED

    0x0020

    方法是否是synchronized

    ACC_BRIDGE

    0x0040

    方法是否是由编译器产生的桥接方法

    ACC_VARARGS

    0x0080

    方法是否接受不定参数

    ACC_NATIVE

    0x0100

    方法是否是native

    ACC_ABSTRACT

    0x0400

    方法是否是abstract

    ACC_STRICTFP

    0x0800

    方法是否是strictfp

    ACC_SYNTHETIC

    0x1000

    方法是否是由编译器自动产生的

    本部分二进制流内容:


    从这里可以看到,方法表集合中一共有3个方法,按照字段的解析方法,可以得到每个方法的定义。分别是:

    public void <init>();

    public int inc();

    static void <clinit>();

    可是我们的代码里只定义了一个inc方法,怎么会多出来两个方法?

    其实,Java类都要有一个构造方法,如果没有的话编译器会自动构造一个无参的构造方法,就是上面的第一个名叫<init>的方法;同时,如果一个类中含有静态代码块或者静态变量,那么就需要首先执行类的构造方法,来执行静态代码块和初始化静态变量,这就是上面的第三个名为<clinit>的方法。

    不过,方法比字段还多了方法体呢,那方法体中的代码哪去了?

    在每一个方法表中descriptor_index后描述属性的时候,0001表明属性的个数为1,再后面的000E是指向常量池中的CONSTANT_Utf8_info常量,内容是Code,说明后面属性中存放的就是方法体里面编译后的字节码指令。

    在Java中,要重载一个方法,除了要与原方法具有相同的方法名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是特征签名只包含参数个数和类型,并不包含返回值类型,所以Java语言中是无法仅仅依靠返回值的不同来对一个方法重载的。但是在class文件格式中,特征签名还包括返回值类型,也就是说只有返回值类型不同的两个方法也可以存在。这一点在泛型中编译后类型擦除后生成的桥接方法上有所体现。不过这里就不过多介绍了。

    8、属性表集合

    属性表在前面出现了多次,在class文件、字段表和方法表都可以携带自己的属性表集合,来描述某些场景专有的信息。

    与class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制比较少,不要求严格的顺序,只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自定义的属性信息,Java虚拟机会在运行时忽略掉那些不认识的信息。为了能正确解析class文件,《Java虚拟机规范(第二版)》中预定义了9项虚拟机应当识别的属性。现在,属性已经达到了21项。具体信息如下表,这里仅对常见的属性做介绍:

    属性名称

    使用位置

    含义

    Since

    Code

    方法表

    Java代码编译后的字节码指令

    Java虚拟机规范(第二版)》

    ConstantValue

    字段表

    final关键字定义的常量值

    Java虚拟机规范(第二版)》

    Deprecated

    类、方法表、字段表

    被声明为deprecated的方法和字段

    Java虚拟机规范(第二版)》

    Exception

    方法表

    方法抛出的异常

    Java虚拟机规范(第二版)》

    EnclosingMethod

    类文件

    仅当一个类为局部类或匿名类时才能拥有这个属性,来标识这个类所在的外围方法

    Java虚拟机规范(第二版)》

    InnerClasses

    类文件

    内部类列表

    Java虚拟机规范(第二版)》

    LineNumberTable

    Code属性

    Java源码的行号与字节码指令的对应关系

    Java虚拟机规范(第二版)》

    LocalVariableTable

    Code属性

    方法的局部变量描述

    Java虚拟机规范(第二版)》

    StackMapTable

    Code属性

    供新的类型检查验证器检查和处理目标方法的局部变量和操作数栈所需要的类型检查

    JDK 1.6

    Signature

    类、方法表、字段表

    用于保存泛型中的类型信息

    JDK 1.5

    SourceFile

    类文件

    记录源文件名称

    Java虚拟机规范(第二版)》

    SourceDebugExtension

    类文件

    用于存储额外的调试信息

    Java虚拟机规范(第二版)》

    Synthetic

    类、方法表、字段表

    标识方法或字段为编译器自动生成

    Java虚拟机规范(第二版)》

    LocalVariableTypeTable

    使用特征签名代替描述符,为了描述泛型参数化类型

    JDK 1.5

    RuntimeVisibleAnnotations

    类、方法表、字段表

    为动态注解提供支持

    JDK 1.5

    RuntimeInvisibleAnnotations

    类、方法表、字段表

    RuntimeVisibleAnnotations作用相反

    JDK 1.5

    RuntimeVisbleParameterAnnotations

    方法表

    RuntimeVisibleAnnotations类似

    JDK 1.5

    RuntimeInvisbleParameterAnnotations

    方法表

    RuntimeInvisibleAnnotations类似

    JDK 1.5

    AnnotationDefault

    方法表

    用于记录注解类元素的默认值

    JDK 1.5

    BootstrapMethods

    类文件

    用于保存invokedynamic指令引用的引导方法限定符

    JDK 1.7

    从上表可以看出,属性表集合存在的位置也是不确定的,不仅可以存储在class文件结尾处,还可以作为数据项存在于类、方法表集合和字段表集合中。对于存在于class类文件中的属性表集合很好理解,毕竟在开头的class文件结构图中的最后一部分就是属性表集合,这时属性表集合作为构成class文件结构的一个大部分。剩下的存在于类中、方法表集合与字段表集合中的属性表集合,其实是作为它们的一个数据项存在的。

    存在于类中的属性表集合,存储了关于这个类的一些信息。比如这个类是否是过时的(Deprecated)、在泛型中保存类的类型参数(由于生成class文件后会进行类型擦除,Java中的泛型是一种伪泛型)和动态注解等信息;存放在方法表集合中的属性表集合存储了关于方法的信息,最主要的就是Code属性,存储了字节码指令;存放于字段表集合中的属性表集合存储了关于字段的信息,我们这里的例子没有涉及到字段的属性,不过当在类中定义了静态常量(static final)并且这个常量有初始值时会将这个值作为属性存储在字段表中的属性表集合中。

    由于属性表集合的限制较小,每个属性都会有自己的格式,因此class文件对于属性的格式要求也比较宽松,只需要满足一些特定的条件即可。下表是属性的结构:

    类型

    名称

    数量

    U2

    attribute_name_index

    1

    U4

    attribute_length

    1

    U1

    info

    attribute_length


    从上表可以看出,class文件规定的属性格式只有前6个字节:两个字节的属性名称的索引和4个字节的属性长度,接下来就要按照这个长度存储属性值了。这样的宽松格式使得属性表的结构可以多样变化,甚至可以在属性的内容中再加入一个属性,比较常用的就是方法表集合中的Code属性,在Code属性中还有LineNumberTable属性和LocalVariableTable属性等。

    接下来就简单介绍一下常用的属性。

    (1)Code属性

    最常用的属性恐怕就是Code属性了,因为大多数的方法都会有编译后的字节码指令,这些指令就存储在方法表中的Code属性中。如果一个Java程序的信息可以分为代码(方法体中的代码)和元数据(包括类、字段、方法定义以及其它信息),那么Code属性存储的就是代码,其它所有的结构存储的都是元数据。不过并非所有的方法表都有这个Code属性,比如接口或抽象类中的方法表就不存在Code属性(JDK 1.8中的接口也可以定义方法了)。Code属性的结构如下:

    类型

    名称

    数量

    U2

    attribute_name_index

    1

    U4

    attribute_lenght

    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和attribute_length前面已经介绍过了。

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

    max_locals代表了局部变量表所需要的存储空间。在这里,max_locals的单位是slot,在之前的文章中了解了HotSpot虚拟机在分配对象时使用的单位就是slot。方法参数(包括隐式参数this)、显式异常处理器的参数(try-catch块中catch块中定义的异常)以及方法体中定义的局部变量都需要局部变量表来存放。需要注意的是,由于局部变量表中的slot可以重用,所以并不是所有的局部变量的总slot就是max_locals。编译器会根据变量的作用域来分配slot给各个变量使用,然后计算max_locals的大小。

    code_length和code用来存储字节码指令。Java的字节码指令的长度都是一个字节,即最多可以有256个指令,实际上一共有大约200条指令。对于字节码指令这里不过多介绍。

    (2)SourceFile属性

    本部分的class内容:

    SourceFile属性记录生成这个class文件的源码文件名称。在上面的数据中,0001表示属性表集合中有一个属性,0013(即十进制19)是属性名的索引值,查找常量池可以知道是SourceFile,00000002是这个属性的长度,即两个字节,最后的两个字节就是这个属性的内容,是一个常量池索引,0014,十进制20,结果是Test.java。

    还有很多常用的属性,不过由于篇幅关系就不列出来了。

    这样,我们就分析完了整个Class文件的结构,了解这个文件结构对于理解类加载机制很有帮助。


    展开全文
  • 索引、父类索引与接口索引集合 •字段表集合 •方法表集合 •写在前面 提到java,我们可能第一时间想起的就是那句口号,“一次编写,到处运行”,这体现了java与平台无关的优势,而实现这种特性的的基础,是...

    目录

    •写在前面

    •无符号数和表

    •魔数和Class文件的版本

    •常量池

    •访问标志

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

    •字段表集合

    •方法表集合


    •写在前面

    提到java,我们可能第一时间想起的就是那句口号,“一次编写,到处运行”,这体现了java与平台无关的优势,而实现这种特性的的基础,是通过将java编译成字节码文件,虚拟机可以载入和执行同一种平台无关的字节码,从而实现这个平台无关性。但是,如果我们换一种思路就会发现,既然虚拟机直接载入的是字节码文件,也就是说并不直接执行java文件,这也就是说虚拟机其实并不关心是什么语言编译成的字节码文件,只要你提供给我字节码文件,虚拟机就能执行,这一点便体现了java虚拟机的语言无关性。有很多可以在java虚拟机上运行的语言,比如Glojure、JRuby、Jython、Scala等等,这些语言都是通过编译成字节码文件,在java虚拟机上执行的。说白了,实现语言无关性的基础仍然是虚拟机和字节码储存格式,java虚拟机不和包括java在内的任何语言绑定,它只和“Class文件”这种特定的二进制文件格式关联,class文件中包含了java虚拟机指令集和符号表以及若干的其他辅助信息。值得一提的是,既然无论什么语言,只要编译class文件,虚拟机就可以运行,这也就意味着class文件有着许多强制性的预发和结构化的约束来保证安全性。任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件中,比如类或接口可以通过类加载器直接生成(类加载器看我另一篇文章)。

    Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这也导致整个class文件中储存的内容几乎全部都是程序运行的必要数据,没有空隙存在,当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的分割成若干个8位字节进行储存。

    •无符号数和表

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

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

    表是由多个无符号数或者其他表作为数据项构成的符合数据类型,所有表都习惯性地以“_info”结尾,表用于描述有层次关系的复合结构的数据,整个Class文件本质就是一张表,具体的基本数据项可以看下面这个图。

    无论是无符号数还是表,当需要描述同一个类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一个类型的数据为某一类型的集合。

    下面将具体介绍各数据项的具体细节,在此之前,再一次强调,class文件中由于没有任何分隔符号,所以无论是顺序还是数量,甚至是数据储存的字节序这样的细节,都被严格限定了,哪个字节代表什么含义、长度是多少、先后顺序如何、都不允许改变。后面具体分析的class文件,使用winhex打开,class文件是我随便在以前的项目找的,你们也可以打开一个class进行查看验证。这里我先贴一张总的出来,后面单独数据项分析的时候,我就部分截图了。

    •魔数和Class文件的版本

    每个Class文件的头4个字节称为魔数。它的唯一作用是确定这个文件是否为一个能被虚拟机接受的class文件,可以这么理解,魔数是某个文件格式的标识,很多文件储存标准中,都是使用魔数来进行身份识别,比如图片格式,像gif、jpeg等在文件头中都存有魔数。使用魔数而不是拓展名来进行识别主要是基于安全方面考虑,因为文件拓展名可以随意的改动。所以如果你想要的一个自己的类型格式文件,你可以自由的选择魔数值(当然啦,不要和现有格式混淆),所以class文件的魔数是0xCAFEBABE,看下面图。

    紧接着魔数的4个字节存储的是class文件的版本号,第5和第6个字节是次版本号,第7和第8个字节是主版本号。这里大概说一下java版本号的规则,java的版本号是从45开始的,JDK之后的每个JDK大版本发布主版本号向上加1(它之前的JDK1.0-1.1使用的是45.0-45.3),高版本的JDK能向下兼容以前版本的class文件,但不能运行以后版本的class文件,即使class文件格式没有发生任何变化,虚拟机也必须拒绝执行超过其版本号的class文件。我的是1.8

    •常量池

    紧接着主次版本号之后的是常量池入口,常量池可以理解为class文件之中的资源仓库,它是class文件结构中于其他项目关联最多的数据类型,也是占用class文件空间最大的数据项目之一,同时它还是在class文件中第一个出现的表类型数据项目。由于常量的数量不确定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值。值得一提的是,和java习惯不一样的是,这个容量计数是从1开始的,而不是从0开始了。看我打开的这个class文件的常量池容量是0x0036,即十进制的54,这就代表常量池中有53项常量,索引值为1-53。

    常量池中主要存放两大类常量:字面量和符号引用,字面量如文本字符串、声明为final的常量值,而符号引用则属于编译原理方面的概念,包含了类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。值得一提的是,java代码在进行javac编译的时候,在虚拟机加载class文件的时候进行动态连接的,所以说在class文件中不会保存各个方法、字段的最终内存布局信息。因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址。虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。常量池中的每一项常量都是一个表,大概如下

    我们可以对照表查看常量对应的类型,下图我的里面对应的是0x0A,十进制是10,在表中就是指向声明方法的类描述符。这个字节是标志位,标志的是什么类型,我们已经知道了类型是10,纳闷按照表,接下来的四个字节(分为两个2字节)分别代表的意思看图表。

    后面的常量也是按照这个方式计算出来,当然,如果嫌烦,可以使用javap工具进行分析(可以参考我另一篇文章,JDK的命令行工具)使用javap的-verbose参数输出。内容如下,太长了我就不截图了,我直接黏贴内容过来,看看就好。

    C:\Program Files\Java\jdk1.8.0_191\bin>javap -verbose Main
    Classfile /C:/Program Files/Java/jdk1.8.0_191/bin/Main.class
      Last modified 2019-12-19; size 965 bytes
      MD5 checksum f0e541356c7d5365134c19dd1c16e9ac
      Compiled from "Main.java"
    public class Main
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #10.#32        // java/lang/Object."<init>":()V
       #2 = Class              #33            // com/dbc/leecode/Algorithm/Reclass/ListNode
       #3 = Methodref          #2.#34         // com/dbc/leecode/Algorithm/Reclass/ListNode."<init>":(I)V
       #4 = Fieldref           #2.#35         // com/dbc/leecode/Algorithm/Reclass/ListNode.next:Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
       #5 = Fieldref           #36.#37        // java/lang/System.out:Ljava/io/PrintStream;
       #6 = Integer            -2147483648
       #7 = Methodref          #38.#39        // com/dbc/leecode/Algorithm/Solution21_30/Solution30.divide:(II)I
       #8 = Methodref          #40.#41        // java/io/PrintStream.println:(I)V
       #9 = Class              #42            // Main
      #10 = Class              #43            // java/lang/Object
      #11 = Utf8               <init>
      #12 = Utf8               ()V
      #13 = Utf8               Code
      #14 = Utf8               LineNumberTable
      #15 = Utf8               LocalVariableTable
      #16 = Utf8               this
      #17 = Utf8               LMain;
      #18 = Utf8               main
      #19 = Utf8               ([Ljava/lang/String;)V
      #20 = Utf8               args
      #21 = Utf8               [Ljava/lang/String;
      #22 = Utf8               listNode1
      #23 = Utf8               Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
      #24 = Utf8               listNode2
      #25 = Utf8               listNode3
      #26 = Utf8               listNode4
      #27 = Utf8               listNode5
      #28 = Utf8               s
      #29 = Utf8               [I
      #30 = Utf8               SourceFile
      #31 = Utf8               Main.java
      #32 = NameAndType        #11:#12        // "<init>":()V
      #33 = Utf8               com/dbc/leecode/Algorithm/Reclass/ListNode
      #34 = NameAndType        #11:#44        // "<init>":(I)V
      #35 = NameAndType        #45:#23        // next:Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
      #36 = Class              #46            // java/lang/System
      #37 = NameAndType        #47:#48        // out:Ljava/io/PrintStream;
      #38 = Class              #49            // com/dbc/leecode/Algorithm/Solution21_30/Solution30
      #39 = NameAndType        #50:#51        // divide:(II)I
      #40 = Class              #52            // java/io/PrintStream
      #41 = NameAndType        #53:#44        // println:(I)V
      #42 = Utf8               Main
      #43 = Utf8               java/lang/Object
      #44 = Utf8               (I)V
      #45 = Utf8               next
      #46 = Utf8               java/lang/System
      #47 = Utf8               out
      #48 = Utf8               Ljava/io/PrintStream;
      #49 = Utf8               com/dbc/leecode/Algorithm/Solution21_30/Solution30
      #50 = Utf8               divide
      #51 = Utf8               (II)I
      #52 = Utf8               java/io/PrintStream
      #53 = Utf8               println
    {
      public Main();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 9: 0
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0       5     0  this   LMain;
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=4, locals=7, args_size=1
             0: new           #2                  // class com/dbc/leecode/Algorithm/Reclass/ListNode
             3: dup
             4: iconst_1
             5: invokespecial #3                  // Method com/dbc/leecode/Algorithm/Reclass/ListNode."<init>":(I)V
             8: astore_1
             9: new           #2                  // class com/dbc/leecode/Algorithm/Reclass/ListNode
            12: dup
            13: iconst_2
            14: invokespecial #3                  // Method com/dbc/leecode/Algorithm/Reclass/ListNode."<init>":(I)V
            17: astore_2
            18: new           #2                  // class com/dbc/leecode/Algorithm/Reclass/ListNode
            21: dup
            22: iconst_3
            23: invokespecial #3                  // Method com/dbc/leecode/Algorithm/Reclass/ListNode."<init>":(I)V
            26: astore_3
            27: new           #2                  // class com/dbc/leecode/Algorithm/Reclass/ListNode
            30: dup
            31: iconst_4
            32: invokespecial #3                  // Method com/dbc/leecode/Algorithm/Reclass/ListNode."<init>":(I)V
            35: astore        4
            37: new           #2                  // class com/dbc/leecode/Algorithm/Reclass/ListNode
            40: dup
            41: iconst_5
            42: invokespecial #3                  // Method com/dbc/leecode/Algorithm/Reclass/ListNode."<init>":(I)V
            45: astore        5
            47: aload_1
            48: aload_2
            49: putfield      #4                  // Field com/dbc/leecode/Algorithm/Reclass/ListNode.next:Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
            52: aload_2
            53: aload_3
            54: putfield      #4                  // Field com/dbc/leecode/Algorithm/Reclass/ListNode.next:Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
            57: aload_3
            58: aload         4
            60: putfield      #4                  // Field com/dbc/leecode/Algorithm/Reclass/ListNode.next:Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
            63: aload         4
            65: aload         5
            67: putfield      #4                  // Field com/dbc/leecode/Algorithm/Reclass/ListNode.next:Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
            70: bipush        6
            72: newarray       int
            74: dup
            75: iconst_0
            76: iconst_1
            77: iastore
            78: dup
            79: iconst_1
            80: iconst_0
            81: iastore
            82: dup
            83: iconst_2
            84: iconst_m1
            85: iastore
            86: dup
            87: iconst_3
            88: iconst_0
            89: iastore
            90: dup
            91: iconst_4
            92: bipush        -2
            94: iastore
            95: dup
            96: iconst_5
            97: iconst_2
            98: iastore
            99: astore        6
           101: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
           104: ldc           #6                  // int -2147483648
           106: iconst_m1
           107: invokestatic  #7                  // Method com/dbc/leecode/Algorithm/Solution21_30/Solution30.divide:(II)I
           110: invokevirtual #8                  // Method java/io/PrintStream.println:(I)V
           113: return
          LineNumberTable:
            line 11: 0
            line 12: 9
            line 13: 18
            line 14: 27
            line 15: 37
            line 16: 47
            line 17: 52
            line 18: 57
            line 19: 63
            line 21: 70
            line 22: 101
            line 23: 113
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0     114     0  args   [Ljava/lang/String;
                9     105     1 listNode1   Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
               18      96     2 listNode2   Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
               27      87     3 listNode3   Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
               37      77     4 listNode4   Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
               47      67     5 listNode5   Lcom/dbc/leecode/Algorithm/Reclass/ListNode;
              101      13     6     s   [I
    }
    SourceFile: "Main.java"

    •访问标志

    在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括这个Class是类还是接口、是否定义为public类型、是否定义为abstract类型、如果是类的话,是否声明为final等,具体的看下面的这个表,依旧是对应表去看十六进制数。

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

    类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据集合,class文件中由着三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于java不能多重继承,所以父类索引只有一个,除了java.lang.Object 之外,所有的java类都有父类,因此除了Object外,所有的java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句后的接口顺序依次在接口缩影集合中。类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,他们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义的CONSTANT_Utf8_info类型的常量中的全限定名字符串。整个过程和上面的查表过程是一样的,这里就不重复推了。

    •字段表集合

    字段表用于描述接口或者类中声明的变量,字段包括类级变量以及实力级变量,但不包括在方法内部声明的局部变量。我们可以想一想在java中描述一个字段可以包含什么信息?可以包含的信息有:字段的作用域、实例变量还是类变量、可见性、并发可见性、是否强制从主内存读写、可否被序列化、字段数据类型、字段名称。上面的这些信息都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字、字段被定义了什么数据类型,这些都无法固定,只能引用常量池中的常量来描述

    这里解释一下全限定名、简单名称、描述符三个概念,全限定名和简单名称很好理解,看之前我用javap里面的结果,“com/dbc/leecode/Algorithm/Reclass/ListNode”是这个类的全限定名,仅仅是把类的全名中的"."换成了“/”而已,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般都会加入一个“;”表示全限定名结束。简单名称是指没有类型和参数修饰的方法或者字段名称,这个类中的inc()方法和m字段的简单名称分别是"inc"和“m”。相对于全限定名和简单名称来说,方法和字段的描述符就要复杂一些,描述符的作用是用来描述字段的数据类型、方法的参数列表和返回值,具体看上面的描述符字符含义。

    这里讲一些特殊的类型,数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[][]”类型的二维数组,将被记录为“[[Ljava/lang/String”,一个整型数组“int[]”将被标记为“[I”。描述方法时,按照先参数列表,后返回值的顺序描述,参数列表的参数严格顺序放在一组小括号“()”之内,如方法void inc()的描述符为“()V”,方法java.lang.String toString()的描述符为“()Ljava/lang/String”,方法int indexOf(char[]source, int sourceOffset, int sourceCount, char[]target, int tarOffset, int targetCount, int fromIndex)的描述为“([CII[CIII)I”

    •方法表集合

    方法表的命名和字段表差不多,这里我就不多讲,直接贴上查表。

     

    展开全文
  • java 类文件结构

    千次阅读 2016-05-09 19:55:36
    1 Class文件与虚拟机: ...基于安全方面的考虑,Java 虚拟机规范要求Class文件中使用许多强制性的语法和结构化约束,但任何一门语言都可以表示为一个能被Java虚拟机所接受的有效的Class文件。Java虚拟机
  • java类文件结构

    2014-11-12 15:50:11
    我们都知道我们编写的java程序是要经过编译器编译成class文件,然后由java虚拟机执行class文件,来运行程序的。今天我们简单分析一下class文件结构
  • Java类文件结构

    2008-05-27 17:52:00
    ASM_Guide - 学习笔记1 - Part 1 Core API:2. Classess: 2.1 Structure : 2.1.1 Overview: 1... 一个类文件包含以下几部分的内容(In fact a compiled class contains): A section describing the modifiers(such as
  • Class类文件结构之ConstantValue属性

    千次阅读 多人点赞 2016-07-06 00:03:29
    编程过程中经常用的static,final关键字,也经常听到同事谈起这些词,到底他们怎么样呢,jvm中class类文件结构可以找到答案。ConstantValue属性ConstantValue属性的作用是通知虚拟机自动为静态变量赋值,只有被...
  • 无关性的基石 各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码(ByteCode)是构成平台无关性的基石。实现语言无关性的基础仍然是虚拟机和字节码存储格式。...Class类文件结构 任何一个Clas
  • Java类文件结构详解

    千次阅读 2017-09-14 10:29:23
    Class文件是一组以8位字节为基础单位的二进制流,各项数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,如果是超过8位字节以上空间的数据项,则会按照高位在前的方式(Big-Endian)分割成...
  • 深入理解JVM之五:类文件结构

    千次阅读 2015-11-16 13:53:21
    前言我们平时在DOS界面中往往需要运行先运行javac命令,这个命令的直接结果就是产生相应的class文件,然后基于这个class文件才可以真正运行程序得到结果。自然。这是Java虚拟机的功劳,那么是不是Java虚拟机只能编译...
  • JVM之类文件结构

    千次阅读 2019-08-13 23:02:24
    1.java是一门跨平台的语言,那么跨平台到底是怎么实现的 代码编译的结果 从本地机器码转化为字节码,是存储格式发展的一小步,确实编程语言的一大步。计算机虽然只能识别0和1,但是将编写的程序编译成二进制本地...
  • 解读Class类文件结构

    千次阅读 2018-04-29 18:16:16
    一、Class文件结构 任何一个Class文件都对应着唯一 一个或接口的定义信息,但反过来说,或接口并不一定都得定义在文件里(譬如或接口也可以通过加载器直接生成)。 问题: 当出现超...
  • Class类文件结构浅析

    千次阅读 2014-04-28 22:32:30
     class文件时java虚拟机执行引擎的数据入口,也是java技术体系的基础支柱之一,了解class文件结构对后面进一步了解虚拟机执行引擎有很重要的意义。 概要:  class文件是一组以八位字节为基础单位的二进制流,...
  • JVM:类文件结构

    千次阅读 2018-04-28 14:55:25
    任何一个Class文件都对应唯一一个或接口的定义信息。,但是反过来说,或者接口不一定定义在文件中,有可能通过加载器直接生成。Class文件以8字节为基本单位二进制流存储,当遇到8字节以上时,会按照高位在前的...
  • 8.类文件结构

    千次阅读 2018-04-21 15:35:11
    Class类文件的数据结构 任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(比如,类或接口也可以通过类加载器直接生成); Class文件是一组以8位字节为基础单位...
  • Java类文件结构笔记

    2015-03-22 12:31:11
    概述 一次编写,处处运行(Write Once, Run Anywhere). 各种平台的虚拟机与所有平台都统一使用的程序存储格式-字节码(ByteCode). ...Class类文件结构Class文件是一组以8字节为基础单位的二进制流,中间没有
  • 7.类文件结构

    2017-10-17 09:01:58
    1.概述 实现语言无关性的基础是虚拟机和字节码存储格式。 Java虚拟机不和包括Java在内的任何语言绑定, 它只与“Class文件” 这种特定的二进制文件格式所关联, Class...2.class文件结构 Class文件是一组以8位字节为
  • java Class类文件结构

    2014-10-29 23:34:32
    不仅使用Java编译器可以把Java代码编译成存储字节码的Class文件,使用JRuby等其他语言的编译器也可以把程序代码编译成Class文件,虚拟机并不关心Class的来源是什么语言,只要它符合一定的结构,就可以在Java中运行。...

空空如也

1 2 3 4 5 ... 20
收藏数 30,486
精华内容 12,194
关键字:

类文件结构