Java虚拟机(四)-Class文件结构

By prince No comments

1.Java语言的平台无关性和JVM的语言无关性

Java语言的平台无关性

《深入理解Java虚拟机-第二版》中第6章开头就写到:

代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。

为什么这么说呢?作者下面也解释的很明白。因为在虚拟机出现之前,程序要想正确运行在计算机上,首先要将代码编译成二进制本地机器码,而这个过程是和电脑的操作系统OS、CPU指令集强相关的,所以可能代码只能在某种特定的平台下运行,而换一个平台或操作系统就无法正确运行了。随着虚拟机的出现,直接将程序编译成机器码,已经不再是唯一的选择了。越来越多的程序语言选择了与操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。Java就是这样一种语言。“一次编写,到处运行”于是成立Java的宣传口号。

正是虚拟机和字节码(ByteCode)构成了平台无关性的基石,从而实现“一次编写,到处运行”

Java虚拟机将.java文件编译成字节码,而.class字节码文件经过JVM转化为当前平台下的机器码后再进行程序执行。这样,程序猿就无需重复编写代码来适应不同平台了,而是一套代码处处运行,至于字节码怎样转化成对应平台下的机器码,那就是Java虚拟机的事情了。

JVM的语言无关性

Java语言通过JVM虚拟机和字节码(ByteCode)实现了平台无关性,那么语言无关性又是什么意思?其实,在Java虚拟机设计之初,作者非常前瞻性的说过:

“In the future,we will consider bounded extensions to the Java virtual machine to provide better support for other languages” 在未来,我们会对java虚拟机进行适当的拓展,以便更好的支持其他语言运行于JVM之上。

时至今日,商业机构和开源机构以及在Java语言之外发展出一大批在Java虚拟机之上运行的语言,如Groovy,JRuby,Jython,Scala等等。这些语言通过各自的编译器编译成为.class文件,从而可以被JVM所执行。

所以,由于Java虚拟机设计之初的定位,以及字节码(ByteCode)的存在,使得JVM可以执行不同语言下的字节码.class文件,从而构成了语言无关性的基础。或许在未来,语言无关性的优势会赶超Java平台无关性的优势。

2.字节码.class文件的结构

知识准备

在详细分析class文件结构之前,我们需要了解一些基本概念:

  • class文件以8字节为基本单位来进行存储,中间没有任何分隔符;
  • 当数据项需要占用的空间大于8字节时,会按照高位在前的方式来进行分割;
  • class文件只有两种数据类型:无符号数、表;
  • 无符号数属于基本数据类型,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节和8个字节的无符号数;
  • 表是由多个无符号数或者其它表作为数据项构成的符合数据类型,表名习惯性都以 _info 结尾。

因此本质上整个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 具体属性

可以看到,这16种数据项大致可以分为3类:

  • 3个描述文件属性的数据项:魔数和主次版本号
  • 11个描述类属性的数据项:类、字段、方法等信息
  • 2个描述代码属性的数据项

接下来我们就逐一来看看这些数据项的含义。整个分析过程我们将以下面这段代码对应的class文件为基础:

public class JavaTest {
    private static String name = "JVM";
    public static void main(String[] args) {
        System.out.println("Hello " + name);
    }
}

 

1.魔数(4个字节)

每个class文件的头4个字节称为魔数,用于确定这个文件是否能被虚拟机所接受。值为:0xCAFEBABE-(4个字节)

2.版本号(4个字节)

第5、6字节为次版本号,7、8字节为主版本号。Java的主版本号从45开始,JDK1.1之后每个大版本发布,主版本号加1。高版本的jdk能前向兼容之前版本的class文件,但不能运行以后版本的class文件。

  • minor_version 次版本号  (2个字节)
  • major_version 主版本号   (2个字节)
  • JDK向下兼容,向上不兼容

3. 常量池

  • constant_pool_count:常量池容量计数值(2个字节)
    • 计数从1开始,第0项空出来
    • 常量池计数器 = 常量池表中的成员数 + 1. 常量池的索引值index,只有在index > 0 && index <constant_pool_count时才会认为是有效的
  • 常量池主要用来存放字面量(Literal)和符号引用(Symbolic Reference)
    • 字面量如文本字符串,声明为final的常量
    • 符号引用包括:
      • 类和接口的全限定名(Full Qualified Name)
      • 字段的名称和描述符(Descriptor)
      • 方法的名称和描述符
  • 常量池中每一个常量都是一个表
    • 表结构各不相同
    • 都有一个u1类型的标志位(tag),代表当前常量属于的数据类型
  • javap -verbose Class可以输出常量表,例如
javap -v JavaTest.class
Classfile /Users/xxx/Downloads/JavaTest.class
  Last modified 2018-3-17; size 842 bytes
  MD5 checksum fbb2370c6b7413a0636806a0e492224a
  Compiled from "JavaTest.java"
public class com.princeli.JavaTest
  minor version: 0
  major version: 49
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #12.#29        // java/lang/Object."<init>":()V
   #2 = Fieldref           #30.#31        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Class              #32            // java/lang/StringBuilder
   #4 = Methodref          #3.#29         // java/lang/StringBuilder."<init>":()V
   #5 = String             #33            // Hello
   #6 = Methodref          #3.#34         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #7 = Fieldref           #11.#35        // com/princeli/JavaTest.name:Ljava/lang/String;
   #8 = Methodref          #3.#36         // java/lang/StringBuilder.toString:()Ljava/lang/String;
   #9 = Methodref          #37.#38        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #10 = String             #39            // JVM
  #11 = Class              #40            // com/princeli/JavaTest
  #12 = Class              #41            // java/lang/Object
  #13 = Utf8               name
  #14 = Utf8               Ljava/lang/String;
  #15 = Utf8               <init>
  #16 = Utf8               ()V
  #17 = Utf8               Code
  #18 = Utf8               LineNumberTable
  #19 = Utf8               LocalVariableTable
  #20 = Utf8               this
  #21 = Utf8               Lcom/princeli/JavaTest;
  #22 = Utf8               main
  #23 = Utf8               ([Ljava/lang/String;)V
  #24 = Utf8               args
  #25 = Utf8               [Ljava/lang/String;
  #26 = Utf8               <clinit>
  #27 = Utf8               SourceFile
  #28 = Utf8               JavaTest.java
  #29 = NameAndType        #15:#16        // "<init>":()V
  #30 = Class              #42            // java/lang/System
  #31 = NameAndType        #43:#44        // out:Ljava/io/PrintStream;
  #32 = Utf8               java/lang/StringBuilder
  #33 = Utf8               Hello
  #34 = NameAndType        #45:#46        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #35 = NameAndType        #13:#14        // name:Ljava/lang/String;
  #36 = NameAndType        #47:#48        // toString:()Ljava/lang/String;
  #37 = Class              #49            // java/io/PrintStream
  #38 = NameAndType        #50:#51        // println:(Ljava/lang/String;)V
  #39 = Utf8               JVM
  #40 = Utf8               com/princeli/JavaTest
  #41 = Utf8               java/lang/Object
  #42 = Utf8               java/lang/System
  #43 = Utf8               out
  #44 = Utf8               Ljava/io/PrintStream;
  #45 = Utf8               append
  #46 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #47 = Utf8               toString
  #48 = Utf8               ()Ljava/lang/String;
  #49 = Utf8               java/io/PrintStream
  #50 = Utf8               println
  #51 = Utf8               (Ljava/lang/String;)V
常量池表constant_pool

常量池是一种表结构,它包含了Class文件结构及其及其子结构中引用的所有字符串常量,类或者接口名、字段名或者其他常量。常量池中的所有项都具有如下通用格式:

cp_info {
    u1 tag; //表示cp_info的单字节标记位
    u1 info[]; //两个或更多的字节表示这个常量的信息,信息格式由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_InterfaceMehtodref_info 11 接口方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的部分符号引用
CONSTANT_MethodHandle_info 15 方法句柄
CONSTANT_MethodType_info 16 方法类型
CONSTANT_InvokeDynamic_info 18 动态方法调用点

这14种常量的结构如下表所示:

举几个典型的例子来说明常量池中数据是如何存储的:
CONSTANT_Class结构 — 表示类或者接口,他的格式如下:
CONSTANT_Class_info {
     u1 tag; //这个值为 CONSTANT_Class (7)
     u2 name_index;//注意这是一个index,他表示一个索引,引用的是    CONSTANT_UTF8_info
}

注意观察 这个CONSTANT_Class_info类型的常量内部结构是由一个tag(CONSTANT_Class(7))和一个name_index组成,name_index中注意这个index,他表示一个索引的,什么的索引呢?CONSTANT_Utf8_info结构的索引,这个结构用来表示一个有效的类或者接口的二进制名称的内部形式。class文件结构中出现的类或者接口名称都是通过全限定形式来表示的,也被称作二进制名称(题外话:全限定类名含义就类似 java.lang包中定义的Object类的完全限定名称为java.lang.Object)。

那我们接着看CONSTANT_Utf8_info结构,他用于表示字符常量的值,他的结构如下所示:

CONSTANT_Utf8_info {
     u1 tag;
     u2 length;
     u1 bytes[length];
}
我们注意到第一个tag肯定表示为:CONSTANT_Utf8(1);后面的length指明了bytes[]数组的长度;最后一个bytes[]数组引用了上一个length作为其长度。字符常量采用改进过的UTF-8编码表示。

4. 访问标志

紧接着常量池之后的两个字节表示访问标志,主要是用来标记类或者接口层次的一些属性。目标之定义了16个标志位中的8位,没有使用到的一律为0。 具体标志位如下表:

标志名称 标志值 描述
ACC_PUBLIC 0x0001 是否为public类型
ACC_FINAL 0x0010 是否为final类型
ACC_SUPER 0x0020 是否允许使用invokespcial字节码指令的新语义,jdk1.0.2之后编译出来的类,此标志都为真
ACC_INTERFACE 0x0200 是否为接口
ACC_ABSTRACT 0x0400 是否为abstract类型(对接口和抽象类来说,此标志都为真)
ACC_SYNTHETIC 0x1000 标识这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 是否是注解
ACC_ENUM 0x4000 是否是枚举

包括这个Class是接口还是类,是否定义为public类型,是否定义为abstract类型,如果是类的话是否声明为final.

 

5. 类索引、父类索引和接口索引集合

在访问标志之后,有3个用来确定一个类的继承关系的数据,按先后顺序分别是:

  • 类索引:用于确定类的全限定名

this_class 的值必须是对 constant_pool 表中项目的一个有效索引值。constant_pool 表在这个索引处的项必须为 CONSTANT_Class_info 类型常量,表示这个 Class 文件所定义的类或接口。

由此可见,类索引为11,根据前面得到的常量池,可以知道第11个常量为:

...
#11 = Class  #40 // com/princeli/JavaTest
...
#40 = Utf8   com/princeli/JavaTest
...

上面name_index指向下面CONSTANT_Utf8_info的索引。

  • 父类索引:用于确定父类的全限定名

super_class 的值必须为 0 或者是对 constant_pool 表中项目的一个有效索引值。如果它的值不为 0,那 constant_pool 表在这个索引处的项

必须为 CONSTANT_Class_info 类型常量,表示这个 Class 文件所定义的类的直接父类。当前类的直接父类,以及它所有间接父类的 access_flag 中都不能带有 ACC_FINAL 标记。对于接口来说,它的 Class 文件的 super_class 项的值必须是对 constant_pool 表中项目的一个有效索引值。constant_pool 表在这个索引处的
项必须为代表 java.lang.Object 的 CONSTANT_Class_info 类型常量。如果 Class 文件的 super_class 的值为 0,那这个 Class 文件只可能是定义的是
java.lang.Object 类,只有它是唯一没有父类的类。

由此可见,类索引为12,根据前面得到的常量池,可以知道第12个常量为:

...
#12 = Class #41 // java/lang/Object
...
#41 = Utf8 java/lang/Object
...

类索引this_class、父类索引super_class都是一个u2类型的数据

  • 接口索引:用于描述类实现了哪些接口

接口索引集合包含一个u2类型的接口计数项intefaces_count和若干个u2类型的数据集合。

interfaces_count
接口计数器,interfaces_count 的值表示当前类或接口的直接父接口数量。

 interfaces[]
接口表,interfaces[]数组中的每个成员的值必须是一个对 constant_pool 表中项目的一个有效索引值,它的长度为 interfaces_count。每个成员 interfaces[i] 必须为 CONSTANT_Class_info 类型常量 ,其中 0 ≤ i <interfaces_count。在 interfaces[]数组中,成员所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,即 interfaces[0]对应的是源代码中最左边的接口。

6. 字段表集合

fields_count
字段计数器,fields_count 的值表示当前 Class 文件 fields[]数组的成员个数。fields[]数组中每一项都是一个 field_info 结构的数据项,它用于表示该类或接口声明的类字段或者实例字段 。

注意::类字段即被声明为 static 的字段,也称为类变量或者类属性,同样,实例字段是指未被声明为static 的字段。由于《Java 虚拟机规范》中,“Variable”和“Attribute”出现频率很高且在大多数场景中具备其他含义,所以译文中统一把“Field”翻译为“字段”,即“类字段”、“实例字段”。

fields[]
字段表,fields[]数组中的每个成员都必须是一个 fields_info 结构的数据项,用于表示当前类或接口中某个字段的完整描述。fields[]数组描述当前类或接口
声明的所有字段,但不包括从父类或父接口继承的部分。

字段表的格式如下:

类型 名称 数量 含义
u2 access_flags 1 字段修饰符
u2 name_index 1 字段和方法简单名称在常量池中的引用
u2 descriptor_index 1 字段和方法描述符在常量池中的引用
u2 attributes_count 1 描述字段额外信息属性的个数
attribute_info attributes attributes_count 具体描述字段的额外信息属性
field {
  u2 access_flags;
  u2 name_index;
  u2 descriptor_index;
  u2 attributes_count;
   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_TRANSIENT 0x0080 是否transient类型
ACC_SYNTHETIC 0x1000 是否由编译器自动产生
ACC_ENUM 0x4000 是否enum类型

name_index

字段名字,必须指向一个CONSTANT-UTF8类型的结构

descriptor_index

字段描述符,必须指向一个CONSTANT-UTF8类型的结构

描述符用来描述字段的数据类型、方法的参数列表和返回值。其中基本类型字段的描述符用一个大写字母来表示,而对象类型则用字符L加上对象类型的全限定名来表示。具体如下表:

描述符 含义
B 基本类型byte
C 基本类型char
D 基本类型double
F 基本类型float
I 基本类型int
J 基本类型long
S 基本类型short
Z 基本类型boolean
V 基本类型void
L 对象类型,如Ljava/lang/Object

对于数组类型,每一个维度都是用一个前置的“[”来描述,如java.lang.String[][]类型的二位数组将被记录为[[java/lang/String;

描述方法时,将按照先参数列表、后返回值的顺序来描述。其中参数列表严格按照参数的顺序放在一组小括号()之内。例如方法java.lang.String.toString()的描述符为()Ljava/lang/String;

attributes_count,attribute_info 

attribute字段,用来描述该变量的属性,因为这个变量没有附加属性,所以attributes_count为0,attribute_info为空。

6.方法表集合

 methods_count
方法计数器,methods_count 的值表示当前 Class 文件 methods[]数组的成员个数。Methods[]数组中每一项都是一个 method_info 结构的数据项。

methods[]
方法表,methods[]数组中的每个成员都必须是一个 method_info 结构(§4.6)的数据项,用于表示当前类或接口中某个方法的完整描述。如果某个 method_info 结构
的 access_flags 项既没有设置 ACC_NATIVE 标志也没有设置 ACC_ABSTRACT 标志,那么它所对应的方法体就应当可以被 Java 虚拟机直接从当前类加载,而不需要引用其它类。method_info 结构可以表示类和接口中定义的所有方法,包括实例方法、类方法、实例初始化方法方法和类或接口初始化方法方法。methods[]数组
只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。
方法表的格式如下:

类型 名称 数量 含义
u2 access_flags 1 方法修饰符
u2 name_index 1 方法简单名称在常量池中的引用
u2 descriptor_index 1 方法描述符在常量池中的引用
u2 attributes_count 1 描述方法额外信息属性的个数
attribute_info attributes attributes_count 具体描述方法的额外信息属性
method {
   u2 access_flags;
   u2 name_index;
   u2 descriptor_index;
   u2 attributes_count;
   attribute_info attributes[attributes_count];
}

access_flags

方法访问标志,表示方法的访问权限和基本属性,比如是public 是不是 final 是不是抽象等,其具体访问标志如下:

标志名称 标志值 描述
ACC_PUBLIC 0x0001 是否为public类型
ACC_PRIVATE 0x0002 是否为final类型
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 是否由编译器自动产生

name_index

方法名字,必须指向一个CONSTANT_UTF8类型的结构

descriptor_index

方法描述符,必须指向一个CONSTANT_UTF8类型的结构

attributes_count,attribute_info 

方法的实际代码存放在属性表attribute_info中的“Code”属性中。

7. 属性表集合

ttributes_count
属性计数器,attributes_count 的值表示当前 Class 文件 attributes 表的成员个数。attributes 表中每一项都是一个 attribute_info 结构(§4.7)的数据项。

attributes[]
属性表,attributes 表的每个项的值必须是 attribute_info 结构。在本规范里,Class 文件结构中的 attributes 表的项包括下列定义的属性:
InnerClasses、EnclosingMethod、Synthetic、Signature、SourceFile,SourceDebugExtension、Deprecated、RuntimeVisibleAnnotations、RuntimeInvisibleAnnotations以及BootstrapMethods属性。

对于支持 Class 文件格式版本号为 49.0 或更高的 Java 虚拟机实现,必须正确识别并读取 attributes 表中的 Signature、RuntimeVisibleAnnotations和RuntimeInvisibleAnnotations属性。

对于支持 Class 文件格式版本号为 51.0 或更高的 Java 虚拟机实现,必须正确识别并读取 attributes 表中的BootstrapMethods属性。

本规范要求任一 Java 虚拟机实现可以自动忽略 Class 文件的 attributes 表中的若干(甚至全部)它不可识别的属性项。任何本规范未定义的属性不能影响 Class 文件的语义,只能提供附加的描述信息。

属性表集合用于描述某些场景的专有信息,它一共有21个属性,属性表结构如下:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 info attribute_length

熟悉了这些属性,学习了class文件结构,这时再用javap -v xxx.class命令来反编译一下字节码,看上去就清晰多了。

发表评论

 

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据