Skip to content

一、Class 文件定义

  • class 文件: 一组以 8 个字节 为基础单位的二进制流。
  • 各个数据项目严格 按照顺序 紧凑地排列在文件之中。中间没有添加任何分隔符,这使得整个 Class 文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在 这样可以使得 class 文件非常紧凑, 体积轻巧, 可以被 JVM 快速的加载至内存, 并且占据较少的内存空间(方便于网络的传输)。
  • class文件: 中的信息是一项一项排列的, 每项数据都有它的 固定长度, 有的占一个字节, 有的占两个字节, 还有的占四个字节或 8 个字节, 数据项的不同长度分别用 u1, u2, u4, u8 表示, 分别表示一种数据项在 class 文件中占据一个字节, 两个字节, 4 个字节和 8 个字节。
类型名称说明数量
u4magic魔数:识别 Class 文件格式1
u2minor_version次版本数1
u2major_version主版本数1
u2constant_pool_count常量池计数器1
cp_infoconstant_pool常量池表constant_pool_count-1
u2access_flags访问标识1
u2this_class类索引1
u2super_class父类索引1
u2interfaces_count接口计数器1
u2interfaces接口索引集合interfaces_count
u2fields_count字段计数器1
field_infofields字段表fields_count
u2methods_count方法计数器1
method_infomethods方法表methods_count
u2attributes_count属性计数器1
attribute_infoattributes属性表attributes_count

二、Class 文件组成部分

  • TestClass.java 代码如下:
java
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.calvin.example;

public class TestClass {
    private int m;

    public TestClass() {
    }

    public int inc() {
        return this.m + 1;
    }

    public static void main(String[] args) {
        TestClass testClass = new TestClass();
        int inc = testClass.inc();
        System.out.println("TestClass.main => inc: " + inc);
    }
}
  • 查看TestClass.class二进制信息
image
  • 通过 javap -v TestClass.class 命令,查看 jvm 字节码文件内容。
shell
Classfile /Users/calvin/学习/01-后端开发/Java/02-进阶知识/Jvm 虚拟机/calvin-java-jvm/chapter-01-jvm-class/target/classes/com/calvin/example/TestClass.class
  Last modified 2023-11-13; size 952 bytes
  MD5 checksum 8a70cabe0c1568e5998b3f54c567a9ac
  Compiled from "TestClass.java"
public class com.calvin.example.TestClass
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #14.#33        // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#34         // com/calvin/example/TestClass.m:I
   #3 = Class              #35            // com/calvin/example/TestClass
   #4 = Methodref          #3.#33         // com/calvin/example/TestClass."<init>":()V
   #5 = Methodref          #3.#36         // com/calvin/example/TestClass.inc:()I
   #6 = Fieldref           #37.#38        // java/lang/System.out:Ljava/io/PrintStream;
   #7 = Class              #39            // java/lang/StringBuilder
   #8 = Methodref          #7.#33         // java/lang/StringBuilder."<init>":()V
   #9 = String             #40            // TestClass.main => inc:
  #10 = Methodref          #7.#41         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #11 = Methodref          #7.#42         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  #12 = Methodref          #7.#43         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #13 = Methodref          #44.#45        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #14 = Class              #46            // java/lang/Object
  #15 = Utf8               m
  #16 = Utf8               I
  #17 = Utf8               <init>
  #18 = Utf8               ()V
  #19 = Utf8               Code
  #20 = Utf8               LineNumberTable
  #21 = Utf8               LocalVariableTable
  #22 = Utf8               this
  #23 = Utf8               Lcom/calvin/example/TestClass;
  #24 = Utf8               inc
  #25 = Utf8               ()I
  #26 = Utf8               main
  #27 = Utf8               ([Ljava/lang/String;)V
  #28 = Utf8               args
  #29 = Utf8               [Ljava/lang/String;
  #30 = Utf8               testClass
  #31 = Utf8               SourceFile
  #32 = Utf8               TestClass.java
  #33 = NameAndType        #17:#18        // "<init>":()V
  #34 = NameAndType        #15:#16        // m:I
  #35 = Utf8               com/calvin/example/TestClass
  #36 = NameAndType        #24:#25        // inc:()I
  #37 = Class              #47            // java/lang/System
  #38 = NameAndType        #48:#49        // out:Ljava/io/PrintStream;
  #39 = Utf8               java/lang/StringBuilder
  #40 = Utf8               TestClass.main => inc:
  #41 = NameAndType        #50:#51        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #42 = NameAndType        #50:#52        // append:(I)Ljava/lang/StringBuilder;
  #43 = NameAndType        #53:#54        // toString:()Ljava/lang/String;
  #44 = Class              #55            // java/io/PrintStream
  #45 = NameAndType        #56:#57        // println:(Ljava/lang/String;)V
  #46 = Utf8               java/lang/Object
  #47 = Utf8               java/lang/System
  #48 = Utf8               out
  #49 = Utf8               Ljava/io/PrintStream;
  #50 = Utf8               append
  #51 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #52 = Utf8               (I)Ljava/lang/StringBuilder;
  #53 = Utf8               toString
  #54 = Utf8               ()Ljava/lang/String;
  #55 = Utf8               java/io/PrintStream
  #56 = Utf8               println
  #57 = Utf8               (Ljava/lang/String;)V
{
  public com.calvin.example.TestClass();
    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 10: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/calvin/example/TestClass;

  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 23: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lcom/calvin/example/TestClass;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: new           #3                  // class com/calvin/example/TestClass
         3: dup
         4: invokespecial #4                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: invokevirtual #5                  // Method inc:()I
        12: istore_2
        13: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
        16: new           #7                  // class java/lang/StringBuilder
        19: dup
        20: invokespecial #8                  // Method java/lang/StringBuilder."<init>":()V
        23: ldc           #9                  // String TestClass.main => inc:
        25: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        28: iload_2
        29: invokevirtual #11                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        32: invokevirtual #12                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        35: invokevirtual #13                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        38: return
      LineNumberTable:
        line 27: 0
        line 28: 8
        line 29: 13
        line 30: 38
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      39     0  args   [Ljava/lang/String;
            8      31     1 testClass   Lcom/calvin/example/TestClass;
           13      26     2   inc   I
}

1.magic 魔数

二进制文件 第一行: CAFE BABE

  • 魔数占用四个字节,判断能被 JVM 识别的.class 文件。
  • 在 class 文件开头的四个字节, 存放着 class 文件的魔数。
  • 这个魔数是 class 文件的标志。 它是一个固定的值: 0XCAFEBABE
  • 也就是说他是判断一个文件是不是 class 格式的文件的标准, 如果开头四个字节不是 0XCAFEBABE, 那么就说明它不是 class 文件, 不能被 JVM 识别。

2.minor_version 次版本号 和 major_version 主版本号

二进制文件 第一行: 0000 0034

  • 0000 代表次版本号, 0034 主版本号
  • 对应字节码内容如下:
shell
  public class com.calvin.example.TestClass
    minor version: 0
    major version: 52
  • 高版本的 JDK 能够向下兼容低版本的 Class 文件,虚拟机会拒绝执行超过其版本号的 Class 文件。
  • 根据以上信息可以获得主版本号信息 0x0034 的(3 * 16 + 4)十进制 52,52 对应的 JDK 版本是 JDK8,可以向下兼容 45-51 的 JDK 版本。JDK 版本是从 45 开始的,JDK1.0-1.1 使用了 45.0-45.3 的版本号。

3.constant_pool 常量池

二进制文件 第一行: 003A

  • 常量池的容量为,A 在十进制为 10,所以计算 3*16 + 10 = 58 个。第一个 null 常用了一个常量,索引范围在 1-57。
  • 常量池入口,常量池可以理解为 Class 文件之中的资源仓库。
  • 因为常量池中常量的数量是不固定的,所以在常量池入口需要放置一个 u2 类型 的数据来表示 常量池的容量
  • 「constant_pool_count」: 和计算机科学中计数的方法不一样,这个 容量是从 1 开始而不是从 0 开始计数。之所以将第 0 项常量空出来是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达「不引用任何一个常量池项目」的含义,这种情况可以把索引值置为 0 来表示。
  • 常量池中主要存放两大类常量:字面量和符号引用。
    • 字面量: 比较接近 Java 语言层面的常量概念,如 字符串、声明为 final 的常量值 等。
    • 符号引用: 属于编译原理方面的概念,包括了以下三类常量。
      • 类和接口的全限定名
      • 字段的名称和描述符
      • 方法的名称和描述符

常量池—常量类型和结构

二进制文件 第一行中第一个常量: 0A 000E 0021

  • 0A 为 10 => 标识位为 10,tag 为 u1 => 占用一个字节,属于 CONSTANT_Methodref_info 类中方法的符号引用
  • 000E 为 #14 => index 为 u2,占用两个字节, 指向常量池中的常量池中的位置 class_index 的索引项。
  • 0021 为 #33 => index 为 u2, 占用两个字节,指向名称及类型描述符 nameAndType 的索引项。

对应字节码内容如下:

shell
#1 = Methodref          #14.#33        // java/lang/Object."<init>":()V

二进制文件 第二行中第二个常量: 09 0003 0022

  • 09 为 9 => 标识位为 9,tag 为 u1 => 占用一个字节,属于 CONSTANT_Fieldref_info 类中字段符号引用
  • 0003 为 #3 => index 为 u2,占用两个字节, 指向常量池中的常量池中的位置 class_index 的索引项。
  • 0022 为 #34 => index 为 u2, 占用两个字节,指向名称及类型描述符 nameAndType 的索引项。

对应字节码内容如下:

shell
#2 = Fieldref           #3.#34         // com/calvin/example/TestClass.m:I

二进制文件 第二行中第三个常量: 07 0023

  • 07 为 7 => 标识位为 7,tag 为 u1 => 占用一个字节,属于 CONSTANT_Class_info 类或接口的符号引用
  • 0023 为 #35 => index 为 u2,占用两个字节, 指向全限定名常量项的索引

对应字节码内容如下:

shell
 #3 = Class              #35            // com/calvin/example/TestClass

二进制文件 第二行中第十九个常量: 01 0004 436F 6465

  • 01 为 1 => 标识位为 1,tag 为 u1 => 占用一个字节,属于 CONSTANT_utf8_info utf-8 编码字符串
  • 0004 => 字符串长度为 4。
  • 436F 6465 => 字节为 code字符串。

对应字节码内容如下:

shell
 #19 = Utf8               Code
  • 常量池中每一项常量都是一个表,JDK1.7 之后共有 14 种不同的表结构数据。如下表格所示:
  • u1:一个字节。
  • u2:两个字节。
  • 标志位1: 表示字符串。
  • length: 表示字符串的字节长度。
image

4.access_flags 访问标志

二进制文件 第四十三行: 0021 00

  • access_flag 的值为:0x0001|0x0020 = 0x0021
  • 没有使用到的标志位要求一律为 0。

对应字节码内容如下:

shell
 flags: ACC_PUBLIC
  • 标志用于识别一些类或者接口层次的访问信息。
  • 包括这个 Class 是类还是接口,是否定义为 public 类型, 是否定义为 abstract 类型,如果是类的话,是否被申明为 final 等。
  • 具体的标志位以及标志的含义见下表(access_flags 中一共有 16 个标志位可以使用,当前只定义了其中的 9 个,没有使用到的标志位要求一律为 0。):
标志名称标志值含义
ACC_PUBLIC0x0001是否为 public 类型
ACC_FINAL0x0010是否被声明为 final,只有类可设置
ACC_SUPER0x0020是否允许使用 invokespecial 字节码指令
ACC_INTERFACE0x0200标识这是一个接口
ACC_ABSTRACT0x0400是否为 abstract 类型,对于接口或者抽象类来说,此标志值为真,其它类值为假
ACC_SYNTHETIC0x1000标识这个类并非由用户代码产生
ACC_ANNOTATION0x2000标识这是一个注解
ACC_ENUM0x4000标识这是一个枚举

4.this_class 类索引

二进制文件 第四十四行: 0030

  • 占用 2 个字节,使用 this_class 类描述常量

对应字节码内容如下:

shell
0: new           #3                  // class com/calvin/example/TestClass
  • 一个 u2 类型的数据.
  • 类索引用于确定这个类的全限定名.
  • 指向 CONSTANT_Class_info 的类描述常量,通过 CONSTANT_Class_info 的类型常量中的索引可以找到,CONSTANT_Utf8_info 类型的常量中的全限定名字符串。从而获取到该类的全限定名

5.super_class 父类索引

二进制文件 第四十四行: 000E

  • 占用 2 个字节,使用 super_class 类描述常量
  • 指向的是第 14 个

对应字节码内容如下:

shell
#14 = Class              #46            // java/lang/Object
  • 一个 u2 类型的数据
  • 父类索引用于确定这个类的父类的全限定名
  • 指向 CONSTANT_Class_info的类描述常量,通过 CONSTANT_Class_info 的类型常量中的索引可以找到,CONSTANT_Utf8_info 类型的常量中的全限定名字符串。从而获取到该类的全限定名

6.interfaces 接口索引集合

二进制文件 第四十四行: 0000

  • 父类索引之后为接口索引(interfaces) = 0x0000,因为没有实现任何接口,因此为全 0.
  • 一组 u2 类型的数据集合
  • 接口索引集合用于描述这个类实现了哪些接口
  • Class 文件中由这三项数据(this_class、super_class、interfaces)来确定这个类的继承关系。
  • 类索引、父类索引、接口索引都排在访问标志之后。由于所有的类都是 java.lang.Object 类的子类,因此除了 Object 类之外所有类的父类索引都不为 0。

7.fields 字段表集合

二进制文件 第四十四行: 0001 0002 000F 0010 0000

  • 0001: 表示 fields_count 字段个数 1 个。
  • 0002: 表示 access_flag 访问标识符 0x0002 => ACC_PRIVATE
  • 000F: 表示 name_index 查看常量池表 #15 常量是 CONSTANT_Utf8_info,其值为 m
  • 0010: 表示 descriptor_index 该常量值索引 #16 常量是 CONSTANT_Utf8_info,其值为 I
  • 0000: 表示 attributes_count 属性表计数器为 0, 也就是没有需要额外描述的信息。

对应字节码内容如下:

shell
 #15 = Utf8               m
 #16 = Utf8               I
  • 用于描述接口或者类中声明的变量。
  • 字段(field) 包括类变量实例变量,但不包括方法内部声明的局部变量
  • 字段表的结构,如下:
  • 字段修饰符字段修饰符 放在 access_flags 中可设置的标志符有 ,它与类中的 access_flag 非常相似,都是一个 u2 的数据类型如下:

字段表结构表格

类型名称数量
u2fields_count1
u2access_flag1
u2name_index1
u2descriptor_index1
attribute_infoattributesattributes_count

字段修饰符字段修饰符

标志名称标志值含义
ACC_PUBLIC0x0001字段是否为 public
ACC_PRIVATE0x0002字段是否为 private
ACC_PROTECTED0x0004字段是否为 protected
ACC_STATIC0x0008字段是否为 static
ACC_FINAL0x0010字段是否为 final
ACC_VOLATILE0x0040字段是否为 volatile
ACC_TRANSIENT0x0080字段是否为 transient
ACC_SYNTHETIC0x1000字段是否由编译器自动生成
ACC_ENUM0x4000字段是否为 enum

8.methods 方法表集合

二进制文件 第四十五行: 0003 0001 0011 0012 0001

  • 0003: 表示 methods_count 方法个数 3 个(构造方法、inc 方法、main 方法)。
  • 0001: 表示 access_flag 访问标识符 ACC_PUBLIC。
  • 0011: 表示 name_index 查看常量池表 #17 名为 init 的方法。
  • 0012: 表示 descriptor_index 该常量值索引 #18 代表 "V"的常量。
  • 0001: 表示 attributes_count 方法的属性表集合有 1 项属性。
  • 0013: 表示 attributes_name_index 属性名称的索引值为 0x0013,对应的常量为"Code",说明此属性是方法的字节码描述。

对应字节码内容如下:

shell
 #17 = Utf8               <init>
 #18 = Utf8               ()V
 #19 = Utf8               Code
  • Class 文件中对方法的描述和对字段的描述是完全一致的,方法表中的结构和字段表的结构一样。
  • 因为 volatile 关键字和 transient 关键字不能修饰方法,所以方法表的访问标志中没有 ACC_VOLATILEACC_TRANSIENT
  • 与之相对的 synchronizes、native、strictfp 和 abstract 关键字可以修饰方法,所以方法表的访问标志中增加了 ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP 和 ACC_ABSTRACT 标志。
  • 对于方法里的代码,经过编译器编译成字节码指令后,存放在方法属性表中一个名为 Code 的属性里面。

方法表的结构

类型名称数量
u2access_flag1
u2name_index1
u2descriptor_index1
u2attributes_count1
attribute_infoattributesattributes_count

标志符

标志名称标志值含义
ACC_PUBLIC0x0001方法是否为 public
ACC_PRIVATE0x0002方法是否为 private
ACC_PROTECTED0x0004方法是否为 protected
ACC_STATIC0x0008方法是否为 static
ACC_FINAL0x0010方法是否为 final
ACC_SYNCHRONIZED0x0020方法是否为 synchronized
ACC_BRIDGE0x0040方法是不是有编译器产生的桥接方法
ACC_VARARGS0x0080方法是否接受不定参数
ACC_NATIVE0x0100方法是否为 native
ACC_ABSTRACT0x0400方法是否为 abstract
ACC_STRICT0x0800方法是否为 strictfp
ACC_SYNTHETIC0x1000方法是否由编译器自动生成

9.attributes 属性表集合

  • 在 Class 文件、字段表、方法表中都可以携带自己的属性表(attribute_info)集合,用于描述某些场景专有的信息。
  • 属性表集合不像 Class 文件中的其它数据项要求这么严格,不强制要求各属性表的顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息。
  • Java 虚拟机在运行时会略掉它不认识的属性。