一起学Java虚拟机(二):类文件结构

811 阅读12分钟

一起学Java虚拟机系列

前言

了解JVM是对Java程序员的基本要求,但是有多少同学和我有一样醉心解bug堆布局,忘记了内功修炼,对JVM的理解是零碎的。系统地学习一次JVM也许能让我们在这条路走得更好更远。

无关性

平台无关性

  • “一次编写,到处运行(Write Once,Run Anywhere)”
  • 各种不同平台的Java虚拟机,以及所有平台都统一支持的程序存储格式——字节码(Byte Code)

语言无关性

  • Java虚拟机不与包括Java语言在内的任何程序语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集、符号表以及若干其他辅助信息。
  • Java的规范拆分成了《Java语言规范》(The Java Language Specification)及《Java虚拟机规范(The Java Virtual Machine Specification)两部分
  • 作为一个通用的、与机器无关的执行平台,任何其他语言的实现者都可以将Java虚拟机作为他们语言的运行基础,以Class文件作为他们产品的交付媒介。

image.png

Class类文件的结构

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

“无符号数”和"表"

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

  • 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
  • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视作是一张表。

使用010Editor查看class文件结构

通过010Editor查看HelloWorld.class

public class HelloWorld {

    private static final String HELLO_WORLD = "Hello World!";
    public static void main(String args[]) {
        System.out.println(HELLO_WORLD);
    }
}

下载地址:
www.sweetscape.com/download/01…
下载完成后打开class文件会自动提示安装CLASSAdv.bt插件

image.png

魔数

  • magic 每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为 一个能被虚拟机接受的Class文件,Class文件的魔数取得很有“浪漫气息”,值为0xCAFEBABE

image.png

版本号

  • minorersion 和 major_version 紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)
    每个版本的 JDK 都有自己特定的版本号。高版本的 JDK 向下兼容低版本的 Class 文件,但低版本不能运行高版本的 Class 文件,即使文件格式没有发生任何变化,虚拟机也拒绝执行高于其版本号的 Class 文件

52对应jdk8

常量池

  • u2 constant_pool_count 由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常 量池容量计数值(constant_pool_count)

image.png
需要注意的是,常量池的下标是从 1 开始的,也就代表该 Class 文件具有 36 个常量。那么,为什么下标要从 1 开始呢?目的是为了表示在特定情况下 不引用任何一个常量池项,这时候下标就用 0 表示。

  • u2 constant_pool

常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References),常量池中每一项常量都是一个表。

常量池的数据类型有十几种,各自都有自己的数据结构,但是他们都有一个共有属性 tag。tag 是标志位,标记是哪一种数据结构。

常见常量池类型:

类 型标 志描 述
CONSTANT_Utf8_info1UTF-8 编码的字符串
CONSTANT_Integer_info3整型字面量
CONSTANT_Float_info4浮点型字面量
COSTANT_Long_info5长整型字面量
CONSTANT_Double_info6双精度浮点型字面量
CONSTANT_Class_info7类或接口的符号引用
CONSTANT_String_info8字符串类型字面量
CONSTANT_Fieldref_info9字段的符号引用
CONSTANT_Methodref_info10类中方法的符号引用
CONSTANT_InterfaceMethodref_info11接口中方法的符号引用
CONSTANT_NameAndType_info12字段或方法的部分符号引用
CONSTANT_MethodHandle_info15表示方法句柄
CONSTANT_MethodType_info16标识方法类型
CONSTANT_InvokeDynamic_info18表示一个动态方法调用点
CONSTANT_Module_ino19表示一个模块
CONSTANT_Package_ino20表示一个模块开或者导出的包

我们再来粗略解析一下常量池的第一项常量 image.png

  • tag :10 代表类型CONSTANT_Methodref_info,类中方法的符号引用
  • classIndex : 6 这个是一个常量池索引, 代表第5个数据项(CONSTANT_Methodref_info 的 class_index 指向的数据项永远是 CONSTANT_Class_info)
  • name_and_type_index :23 同样是一个常量池索引

先来看:classIndex:

image.png

  • tag :7代表CONSTANT_Class_info
  • name_index :30 又是一个常量池索引

image.png

  • 数据项29代表了一个CONSTANT_Utf8_info类型,bytes[16]里是字符串的内容,从 010Editor 解析内容可以看到这个字符串是 java/lang/System,表示类的权限定名

回头在来看第一个常量的 name_and_type_index

image.png

  • tag : 12 代表CONSTANT_NameAndType_info
  • name_index 表示字段或者方法的非限定名,这里的值是
  • descriptor_index表示字段描述符或者方法描述符,这里的值是 ()V。

image.png

这样,常量池的第一个数据项就分析完了,后面的每一个数据项都可以按照这样分析

javap工具

剩余部分全部手动分析太雷,我们偷个懒。在JDK的bin目录中,Oracle公司已经为我们 准备好一个专门用于分析Class文件字节码的工具:javap

HP-ProDesk-680-G6-PCI-Microtower-PC:~/DEBUG$ javap -verbose HelloWorld.class 
Classfile /home/mi/DEBUG/HelloWorld.class
  Last modified May 12, 2021; size 641 bytes
  MD5 checksum 1910a4531e5743c190636067d43d4bc4
  Compiled from "HelloWorld.java"
public class com.wang.javavmdemo.HelloWorld
  minor version: 0
  major version: 52
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #3                          // com/wang/javavmdemo/HelloWorld
  super_class: #6                         // java/lang/Object
  interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #6.#23         // java/lang/Object."<init>":()V
   #2 = Fieldref           #24.#25        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Class              #26            // com/wang/javavmdemo/HelloWorld
   #4 = String             #27            // Hello World!
   #5 = Methodref          #28.#29        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #6 = Class              #30            // java/lang/Object
   #7 = Utf8               HELLO_WORLD
   #8 = Utf8               Ljava/lang/String;
   #9 = Utf8               ConstantValue
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               LocalVariableTable
  #15 = Utf8               this
  #16 = Utf8               Lcom/wang/javavmdemo/HelloWorld;
  #17 = Utf8               main
  #18 = Utf8               ([Ljava/lang/String;)V
  #19 = Utf8               args
  #20 = Utf8               [Ljava/lang/String;
  #21 = Utf8               SourceFile
  #22 = Utf8               HelloWorld.java
  #23 = NameAndType        #10:#11        // "<init>":()V
  #24 = Class              #31            // java/lang/System
  #25 = NameAndType        #32:#33        // out:Ljava/io/PrintStream;
  #26 = Utf8               com/wang/javavmdemo/HelloWorld
  #27 = Utf8               Hello World!
  #28 = Class              #34            // java/io/PrintStream
  #29 = NameAndType        #35:#36        // println:(Ljava/lang/String;)V
  #30 = Utf8               java/lang/Object
  #31 = Utf8               java/lang/System
  #32 = Utf8               out
  #33 = Utf8               Ljava/io/PrintStream;
  #34 = Utf8               java/io/PrintStream
  #35 = Utf8               println
  #36 = Utf8               (Ljava/lang/String;)V
{
  public com.wang.javavmdemo.HelloWorld();
    descriptor: ()V
    flags: (0x0001) 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 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/wang/javavmdemo/HelloWorld;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #4                  // String Hello World!
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 7: 0
        line 8: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature我
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"


类文件的剩余部分,先预览一下:

image.png

访问标志

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

标志位及含义表:

标志名称标 志 值含 义
ACC_PUBIC0x0001是否为 public 类型
ACC_FINAL0x0010是否声明为 final
ACC_SUPER0x0020JDK1.0.2 之后编译出来的类这个标志都必须为真
ACC_INTERFACE0x0200是否为接口
ACC_ABSTRACT0x0400是否为 abstract 类型
ACC_SYNTHETIC0x1000标记这个类并非由用户代码产生
ACC_ANNOTATION0x2000是否为注解
ACC_ENUM0x4000是否为枚举类型
ACC_MODULE0x8000是否为模块

HelloWorld是一个普通类,不是接口、注解、枚举类型或者莫模块,并且被public关键字修饰 因此他的access_flag应该为ACC_PUBIC|ACC_SUPER,转换为10进制就是33

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

Class文件中由这三项数据来确定该类型的继承关系。

  • this_class类索引用于确定这个类的全限定名。value指向常量池中的索引。
  • super_class父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。
  • interfaces_count,表示的是该类实现的接口数量。HelloWorld未实现任何接口所以是0。
  • 如果实现了若干接口,这些接口信息将存储在之后的 interfaces[] 之中。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements关键字(如果这个Class文件表示的是一个接口,则应当是extends关键字)后的接口顺序从左到右排列在接口索引集合中。

字段表长度和字段表集合

Java语言中的“字段”(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。

  • fileds_count 表示该类中声明的变量个数
  • filed_info 表示该类中声明的变量信息

private static final String HELLOWORLD = "HelloWorld";

可以看出来,Java中描述一个字段首先是访问范围,是公有的还是私有的,或者受保护的,这个信息决定了字段是否堆特定范围的类可见。 其次是一些关键字修饰的描述信息,是实例变量还是类变量,是否可变,并发可见性,是否可被序列化等,这些关键字包括static、final 、volatile、transient等。 在后面便是字段的数据类型(基本数据类型、数组、对象)和名称。 上述的这些修饰符都是用布尔值来描述的,而数据类型和名称都是不确定的,通常引用常量池的常量来描述。

image.png

方法表长度和方法表集合

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

image.png

方法的定义可以通过访问标志、名称和描述符索引来表述清楚,那么方法中的代码又在哪里呢?
方法里的java代码经过编译器编译成字节码指令后,存放在方法的属性表集合里一个名为“Code”的属性里。

image.png

attribute_name_index 对应一个类型为CONSTANT_Utf8_info的常量索引,常量值固定为“Code”

属性表长度和属性表集合

Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息 image.png

与class文件其它数据项目严格要求顺序长度不同,属性表集合限制相对比较宽松,不要求各个属性表具有严格顺序,只要不与已有属性名重复,任何人实现的编译器均可向属性表写入自己的属性,jvm运行时会自动忽略掉不认识的属性。

java7中定义的属性如下表:

属性名称使用位置含义
Code方法表Java代码编译成的字节码指令
ConstantValue字段表final关键字定义的常量池
Deprecated类,方法,字段表被声明为deprecated的方法和字段
Exceptions方法表方法抛出的异常
EnclosingMethod类文件仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法
InnerClass类文件内部类列表
LineNumberTableCode属性Java源码的行号与字节码指令的对应关系
LocalVariableTableCode属性方法的局部便狼描述
StackMapTableCode属性JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配
Signature类,方法表,字段表用于支持泛型情况下的方法签名
SourceFile类文件记录源文件名称
SourceDebugExtension类文件用于存储额外的调试信息
Synthetic类,方法表,字段表标志方法或字段为编译器自动生成的
LocalVariableTypeTable使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
RuntimeVisibleAnnotations类,方法表,字段表为动态注解提供支持
RuntimeInvisibleAnnotations表,方法表,字段表用于指明哪些注解是运行时不可见的
RuntimeVisibleParameterAnnotation方法表作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法
RuntimeInvisibleParameterAnnotation方法表作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数
AnnotationDefault方法表用于记录注解类元素的默认值
BootstrapMethods类文件用于保存invokeddynamic指令引用的引导方式限定符

关于字段表集合、属性表集合、方法表集合的结构 以及acess_flag的列表 可以像查字典一样查阅《深入理解JAVA虚拟机》6.3小节的内容。我们只需要理解原理。