Class 文件结构分析

本贴最后更新于 1094 天前,其中的信息可能已经事过境迁

概述

在 Java 语言中,Java 虚拟机只能理解 字节码class文件),它不面向任何处理器,不与任何语言绑定,只与 Class文件 这种特定的二进制文件格式所关联。

Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。

另一方面由于 JVM虚拟机 不与任何语言、机器绑定,因而任何语言的实现着都可以将 Java 虚拟机作为语言的运行基础,以 Class 文件作为他们的交付媒介。例如 Clojure(Lisp 语言的一种方言)、GroovyJRuby 等语言都是运行在 Java 虚拟机之上。

下图展示了不同的语言被不同的编译器编译成 .class 文件最终运行在 Java 虚拟机之上的过程:

image.png

Class 文件的结构

根据《Java 虚拟机规范》,Class 文件通过 ClassFile 定义,而且文件结构采用一种类似 c 语言结构体伪结构体。这种伪结构体只有两种两种数据类型:“无符号数”和“表”。

  • 无符号数:属于基本的数据结构,以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节、8 个字节的无符号数。无符号数可以用来描述数字、索引引用、数字量值或者按照 UTF-8 编码构成字符串值
  • :由多个无符号数或者其他表作为数据项构成的复合数据结构,为了便于区分,所有表的命名都习惯以 "_info" 结尾。

在正式开始讲,我们需要说明一点,Class 文件的结构不像 XML 那样的结构化描述语言,它以 8 个字节为基础单位,各个数据项目严格按照顺序紧凑地排列在文件中,中间没有添加任何分隔符号,因而在 Class 的数据项无论是顺序还是数量,甚至数据存储的字节序(大端存储,Big-Endian)这样的细节,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,全部都不允许改变。

ClassFile 的结构如下:

ClassFile {
    u4             magic; //Class 文件的标志
    u2             minor_version;//Class 的小版本号
    u2             major_version;//Class 的大版本号
    u2             constant_pool_count;//常量池的数量
    cp_info        constant_pool[constant_pool_count-1];//常量池
    u2             access_flags;//Class 的访问标记
    u2             this_class;//当前类
    u2             super_class;//父类
    u2             interfaces_count;//接口
    u2             interfaces[interfaces_count];//一个类可以实现多个接口
    u2             fields_count;//Class 文件的字段属性
    field_info     fields[fields_count];//一个类会可以有多个字段
    u2             methods_count;//Class 文件的方法数量
    method_info    methods[methods_count];//一个类可以有个多个方法
    u2             attributes_count;//此类的属性表中的属性数
    attribute_info attributes[attributes_count];//属性表集合
}

通过对 ClassFile 的分析,我们便可以知道 class 文件的组成。

image.png

上边的一些属性什么的,描述都很抽象,因而这里以一段典型的 java 代码产生的 class 文件为基础结合进行讲解。

一段典型的 Java 程序代码如下:

package com.test;

//接口类
interface Car {
    void drive();
}
//实现类
public class BMWCar implements Car{

    private String name;
  
    public BMWCar() {
        name = "宝马";
    }
  
    @Override
    public void drive() {
        System.out.println("BMW car drive." + name);
    }   
}

通过 javac 命令对代码进行编译,生成的 class 文件内容如下:

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 
00000000: CA FE BA BE 00 00 00 3B 00 33 07 00 02 01 00 0F    J~:>...;.3......
00000010: 63 6F 6D 2F 74 65 73 74 2F 42 4D 57 43 61 72 07    com/test/BMWCar.
00000020: 00 04 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F    .....java/lang/O
00000030: 62 6A 65 63 74 07 00 06 01 00 0C 63 6F 6D 2F 74    bject......com/t
00000040: 65 73 74 2F 43 61 72 01 00 04 6E 61 6D 65 01 00    est/Car...name..
00000050: 12 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69    .Ljava/lang/Stri
00000060: 6E 67 3B 01 00 06 3C 69 6E 69 74 3E 01 00 03 28    ng;...<init>...(
00000070: 29 56 01 00 04 43 6F 64 65 0A 00 03 00 0D 0C 00    )V...Code.......
00000080: 09 00 0A 08 00 0F 01 00 06 E5 AE 9D E9 A9 AC 09    .........e..i),.
00000090: 00 01 00 11 0C 00 07 00 08 01 00 0F 4C 69 6E 65    ............Line
000000a0: 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 12 4C 6F    NumberTable...Lo
000000b0: 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65    calVariableTable
000000c0: 01 00 04 74 68 69 73 01 00 11 4C 63 6F 6D 2F 74    ...this...Lcom/t
000000d0: 65 73 74 2F 42 4D 57 43 61 72 3B 01 00 05 64 72    est/BMWCar;...dr
000000e0: 69 76 65 09 00 18 00 1A 07 00 19 01 00 10 6A 61    ive...........ja
000000f0: 76 61 2F 6C 61 6E 67 2F 53 79 73 74 65 6D 0C 00    va/lang/System..
00000100: 1B 00 1C 01 00 03 6F 75 74 01 00 15 4C 6A 61 76    ......out...Ljav
00000110: 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D    a/io/PrintStream
00000120: 3B 07 00 1E 01 00 17 6A 61 76 61 2F 6C 61 6E 67    ;......java/lang
00000130: 2F 53 74 72 69 6E 67 42 75 69 6C 64 65 72 08 00    /StringBuilder..
00000140: 20 01 00 0E 42 4D 57 20 63 61 72 20 64 72 69 76    ....BMW.car.driv
00000150: 65 2E 0A 00 1D 00 22 0C 00 09 00 23 01 00 15 28    e....."....#...(
00000160: 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E    Ljava/lang/Strin
00000170: 67 3B 29 56 0A 00 1D 00 25 0C 00 26 00 27 01 00    g;)V....%..&.'..
00000180: 06 61 70 70 65 6E 64 01 00 2D 28 4C 6A 61 76 61    .append..-(Ljava
00000190: 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 4C 6A    /lang/String;)Lj
000001a0: 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 42    ava/lang/StringB
000001b0: 75 69 6C 64 65 72 3B 0A 00 1D 00 29 0C 00 2A 00    uilder;....)..*.
000001c0: 2B 01 00 08 74 6F 53 74 72 69 6E 67 01 00 14 28    +...toString...(
000001d0: 29 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69    )Ljava/lang/Stri
000001e0: 6E 67 3B 0A 00 2D 00 2F 07 00 2E 01 00 13 6A 61    ng;..-./......ja
000001f0: 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61    va/io/PrintStrea
00000200: 6D 0C 00 30 00 23 01 00 07 70 72 69 6E 74 6C 6E    m..0.#...println
00000210: 01 00 0A 53 6F 75 72 63 65 46 69 6C 65 01 00 0B    ...SourceFile...
00000220: 42 4D 57 43 61 72 2E 6A 61 76 61 00 21 00 01 00    BMWCar.java.!...
00000230: 03 00 01 00 05 00 01 00 02 00 07 00 08 00 00 00    ................
00000240: 02 00 01 00 09 00 0A 00 01 00 0B 00 00 00 3D 00    ..............=.
00000250: 02 00 01 00 00 00 0B 2A B7 00 0C 2A 12 0E B5 00    .......*7..*..5.
00000260: 10 B1 00 00 00 02 00 12 00 00 00 0E 00 03 00 00    .1..............
00000270: 00 0D 00 04 00 0E 00 0A 00 0F 00 13 00 00 00 0C    ................
00000280: 00 01 00 00 00 0B 00 14 00 15 00 00 00 01 00 16    ................
00000290: 00 0A 00 01 00 0B 00 00 00 48 00 04 00 01 00 00    .........H......
000002a0: 00 1A B2 00 17 BB 00 1D 59 12 1F B7 00 21 2A B4    ..2..;..Y..7.!*4
000002b0: 00 10 B6 00 24 B6 00 28 B6 00 2C B1 00 00 00 02    ..6.$6.(6.,1....
000002c0: 00 12 00 00 00 0A 00 02 00 00 00 13 00 19 00 14    ................
000002d0: 00 13 00 00 00 0C 00 01 00 00 00 1A 00 14 00 15    ................
000002e0: 00 00 00 01 00 31 00 00 00 02 00 32                .....1.....2

魔数

    u4             magic; //Class 文件的标志

每一个 Class 文件的头 4 个字节被称为 魔数,它唯一的作用就是确定这个文件是否是一个能够被虚拟机接受的 Class 文件。

其在 class 文件中的具体位置如下图所示:

image.png

Clsss 文件的魔数选的很有浪漫气息,值为 0xCAFFEBABY(咖啡宝贝?)

Class 文件版本号(Minor&Major Version)

    u2             minor_version;//Class 的小版本号
    u2             major_version;//Class 的大版本号

紧跟着魔数的 4 个字节存储的是 CLass 文件的版本号:第 5 和第 6 个字节是次版本号(Minor Version),第 7 和 8 个字节存储的是主版本号(Manjor Version)。

每当 Java 发布大版本(比如 Java 8,Java9)的时候,主版本号都会加 1。你可以使用 javap -v 命令来快速查看 Class 文件的版本号信息。

高版本的 Java 虚拟机可以执行低版本编译器生成的 Class 文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的 Class 文件。 所以,我们在实际开发的时候要确保开发的的 JDK 版本和生产环境的 JDK 版本保持一致。

class 版本号具体位置如下图所示:

image.png

从图中可以看到,我们 class 文件的主版本号是 0x003B,也就是十进制的 59,这个版本说明是可以被 JDK15 及 其以上版本的虚拟机运行。

常量池(Constant Pool)

    u2             constant_pool_count;//常量池的数量
    cp_info        constant_pool[constant_pool_count-1];//常量池

紧接着主、次版本号之后的是常量池的入口,常量池的数量constant_pool_count - 1常量池计数器是从 1 开始计数的,将第 0 项常量空出来是有特殊考虑的,索引值为 0 代表“不引用任何一个常量池项”)。

常量池中主要存放两大类常量:

  • 字面量:比较接近于 Java 语言层面的常量概念,如文本字符串、被声明的 final

"符号引用" :属于编译原理方面的概念,主要包括三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

常量池中,每一项常量都是一个,截止到 JDK13,常量表中共有 17 种不同类型的常量,它们有一个共同的特点即表结构起始的第一位是一个 u1 类型的标志位(tag),代表当前常量属于哪种常量。

17 中常量及其所对应的标志位如下表所示:

类型 标志(tag) 描述
CONSTANT_utf8_info 1 UTF-8 编码的字符串
CONSTANT_Integer_info 3 整形字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 长整型字面量
CONSTANT_Double_info 双精度浮点型字面量
CONSTANT_Class_info 类或接口的符号引用
CONSTANT_String_info 字符串类型字面量
CONSTANT_Fieldref_info 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的符号引用
CONSTANT_MothodType_info 16 标志方法类型
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点

结合前边的 Class 文件:

image.png

可以看到常量池中常量的数量为 0x33 即有 50 个常量(因为从 1 开始计数),通过 javap -v BMWCar 命令可以查看 Class 文件的信息如下:

Classfile /C:/Users/vcjmhg/Desktop/test/com/test/BMWCar.class
  Last modified 2021-4-17; size 748 bytes
  MD5 checksum e3bb3d3eaf56cc12d92423d7b99781d2
  Compiled from "BMWCar.java"
public class com.test.BMWCar implements com.test.Car
  minor version: 0
  major version: 59
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Class              #2             // com/test/BMWCar
   #2 = Utf8               com/test/BMWCar
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Class              #6             // com/test/Car
   #6 = Utf8               com/test/Car
   #7 = Utf8               name
   #8 = Utf8               Ljava/lang/String;
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Methodref          #3.#13         // java/lang/Object."<init>":()V
  #13 = NameAndType        #9:#10         // "<init>":()V
  #14 = String             #15  
  #15 = Utf8   
  #16 = Fieldref           #1.#17         // com/test/BMWCar.name:Ljava/lang/String;
  #17 = NameAndType        #7:#8          // name:Ljava/lang/String;
  #18 = Utf8               LineNumberTable
  #19 = Utf8               LocalVariableTable
  #20 = Utf8               this
  #21 = Utf8               Lcom/test/BMWCar;
  #22 = Utf8               drive
  #23 = Fieldref           #24.#26        // java/lang/System.out:Ljava/io/PrintStream;
  #24 = Class              #25            // java/lang/System
  #25 = Utf8               java/lang/System
  #26 = NameAndType        #27:#28        // out:Ljava/io/PrintStream;
  #27 = Utf8               out
  #28 = Utf8               Ljava/io/PrintStream;
  #29 = Class              #30            // java/lang/StringBuilder
  #30 = Utf8               java/lang/StringBuilder
  #31 = String             #32            // BMW car drive.
  #32 = Utf8               BMW car drive.
  #33 = Methodref          #29.#34        // java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
  #34 = NameAndType        #9:#35         // "<init>":(Ljava/lang/String;)V
  #35 = Utf8               (Ljava/lang/String;)V
  #36 = Methodref          #29.#37        // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #37 = NameAndType        #38:#39        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #38 = Utf8               append
  #39 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #40 = Methodref          #29.#41        // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #41 = NameAndType        #42:#43        // toString:()Ljava/lang/String;
  #42 = Utf8               toString
  #43 = Utf8               ()Ljava/lang/String;
  #44 = Methodref          #45.#47        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #45 = Class              #46            // java/io/PrintStream
  #46 = Utf8               java/io/PrintStream
  #47 = NameAndType        #48:#35        // println:(Ljava/lang/String;)V
  #48 = Utf8               println
  #49 = Utf8               SourceFile
  #50 = Utf8               BMWCar.java
{
  public com.test.BMWCar();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #12                 // Method java/lang/Object."<init>":()V
         4: aload_0
         5: ldc           #14                 // String 
         7: putfield      #16                 // Field name:Ljava/lang/String;
        10: return
      LineNumberTable:
        line 13: 0
        line 14: 4
        line 15: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/test/BMWCar;

  public void drive();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=4, locals=1, args_size=1
         0: getstatic     #23                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: new           #29                 // class java/lang/StringBuilder
         6: dup
         7: ldc           #31                 // String BMW car drive.
         9: invokespecial #33                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
        12: aload_0
        13: getfield      #16                 // Field name:Ljava/lang/String;
        16: invokevirtual #36                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: invokevirtual #40                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        22: invokevirtual #44                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        25: return
      LineNumberTable:
        line 19: 0
        line 20: 25
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      26     0  this   Lcom/test/BMWCar;
}
SourceFile: "BMWCar.java"

首先我们尝试对第一个常量进行解析,首先找到它对应的标志位(表的第一个字节)为 7,查询上边的常量表可知,该常量为一个 CONSTANT_CLASSS_info类或者接口的符号引用

image.png

查询 CONSTANT_class_info 的结构如下:

类型 名称 数量
u1 tag 标志位
u2 name_index 1

tag 位前边我们说了,它是所有表的一个共同特征,用来指明表的类型;

name_index 是常量池的索引值,指向常量池中一个 CONSTANT_Utf8_info 类型常量,代表这个类的全限定名。

image.png

由于第一个常量的 name_index = 2,也就是指向了常量池中的第二个常量

首先可以看到它的 tag=1,是一个 CONSTANT_UTF8_info 类型的常量,该类型的结构表如下图所示:

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

length 属性说明这个 UTF-8 编码的字符串的长度是多少个字节,后边紧跟着 length 字节的连续数据表示一个使用 UTF-8 缩略编码表示的字符串。

说明:此处缩略编码与普通 UTF 编码的区别在于:从'\u0001'到'\u07ff'(相当于 Ascii 编码 1 到 217)使用一个字节编码,从'\u0080'到 '\u007f'之间的字符使用两个字节编码,剩余部分按照普通 UTF-8 编码规则使用三个字节进行编码。

image.png

我们可以看到该字符串长度为 0x000f 即有 15 个字节,然后紧接着 15 个字节构成了该字符串的值:com/test/BMWCar

将前边两个常量结合在一起我们就了解到该类的全限定名为:com/test/BMWCar

其他常量分析与之类似,我们计算出常量池在 class 中所占用的空间位置如下图所示:

image.png

访问标志(Access Flags)

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

具体的标志位及其含义如下表所示:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为 public 类型
ACC_FINAL 0x0010 是否被声明为 final,只有类可设置
ACC_SUPER 0x0020 是否允许使用 invokespecial 字节码指令新语义,JDK1.0.2 之后都为 true
ACC_INTERFACE 0x0200 标志这是个接口
ACC_ABSTRACT 0x0400 是否是 Abstract 类型,对于抽象类或者接口来说为 true,其他情况为 false
ACC_SYNTHETIC 0x1000 标识这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举
ACC_MODULE 0x8000 标识这是一个模块

access_flags 中一共有 16 个标志位可以使用,当前只定义了 9 个,没有使用到的标志位一律为零(工程上的一种冗余设计思想,值得学习 😁😊)。

image.png

image.png

结合我们的 Class 文件,可以看到该文件的 访问标志0x0021 相当于 0x0020 | 0x0001 查询访问标志表可知,该类是一个 public 类型且可以使用 invokespecial 指令新语义的普通类。

类索引(This Class)、父类索引(Super Class)、接口(Interfaces)索引集合

    u2             this_class;//当前类
    u2             super_class;//父类
    u2             interfaces_count;//接口
    u2             interfaces[interfaces_count];//一个类可以实现多个接口

访问标识之后,紧接着的便是类索引、父类索引与接口索引集合。其中类索引、父类索引都是一个 u2 类型数据,而接口索引集合是一个 u2 类型的数据集合,这三项数据构成了 Class 文件的继承关系。

类索引用来确定这个类的全限定名,父类索引用来用于确定这个类的父类的全限定名。由于 Java 不允许多重继承,所以父类索引有只有一个(java.lang.Object 类除外)。

接口索引集合就是用来描述这个类实现了哪些接口,这些接口将按照 implements 关键字(如果这个 Class 文件表示的是一个接口,则应当使用 extends 关键字)后的接口顺序从左到右排列在接口索引集合中。

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

结合前边的 Class 文件,类索引查找全限定名的过程如下图所示:

image.png

首先根据从 Class 索引值为 0x0001 也即指向常量池中的第一个常量,该常量是一个 CONSTANT_Class_info 类型的数据,该数据的权限定名称指向了常量池中的第三个常量,第三个常量的常量值是 com/test/BMWCar,将整个过程结合在一起我们就知道该类文件的全限定名为 com/test/BMWCar

父类索引的查找过程与之类似,此处不再详述,最终可以定位到该类文件的父类为 java/lang/Object

接口索引由于是集合类型,查找过程与类查找过程可能有些许不同:

image.png

首先找到第一个 u2 类型接口计数器,其值为 0x0001 也就是说该类文件实现了一个接口,其接口索引为 0x0005 即接口索引指向常量池中第五个常量。常量 #5 为一个 CONSTAN_Class_info 类型的常量,指向第六个 CONSTANT_Utf8_info 类型的常量 #6,该常量的值为 com/test/Car

image.png

整个分析下来,我们可以得到该 Class 文件是一个实现了一个全限定名为 com/test/Car 接口的类。

字段表集合(Fields)

    u2             fields_count;//Class 文件的字段的个数
    field_info     fields[fields_count];//一个类会可以有个字段

接口索引后边紧跟着的就是字段表信息,字段表(field info)用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量

字段表的结构如下表所示:

类型 名称 数量 备注
u2 access_flags 1 字段的作用域(public、private、protected 修饰符),是实例变量还是类变量,可否被序列化(transient 修饰符),可变性(final),可见性(volatitle 修饰符,是否强制从主内存读写)
u2 name_index 1 对常量池的引用,表示字段的简单名称
u2 descriptor_index 1 对常量池的引用,表示字段和方法的描述符
u2 attributes_count 1 一个字段可能会额外拥有一些属性,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_FIANL 一起使用
ACC_TRANSIENT 0x0080 在序列化中被忽略的字段
ACC_SYNTHETIC 0x1000 由编译器产生,不存在于源代码中
ACC_ENUM 0x4000 enum

紧随 access_flags 标志的是 name_indexdescriptor_index,他们都是对常量池的引用。name_index 代表着字段的简单名称,descriptor_index 代表着字段的描述符。相比于全限定名和简单名称,方法和字段的描述符要复杂一些。

描述符的主要作用是用来描述字段的数据类型、方法和参数列表(包括数量类型以及顺序)和返回值。因而描述符在设计时,设计了一系列描述规则:

  1. 基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的 void 类型都用一个大写字符来表示。
  2. 对象类型则用字符 L 加上对象的全限定名来表示

描述符标识字含义如下表所示:

标识字符 含义
B 基本类型 byte
C 基本类型 char
D 基本类型 double
F 基本类型 float
I 基本类型 int
J 基本类型 long
S 基本类型 short
Z 基本类型 boolean
V 特殊类型 void
L 对象类型,如 Ljava/lang/Object;
[ 数组类型,多个维度则有多个[

用描述符来描述方法时,按照先参数列表后返回值的顺序描述,参数列表按照参数的严格顺序放在一组“()”之内。

例如方法 int getAge() 的描述符为“()I”,方法 void print(String msg) 的描述符为“(Ljava/lang/String;)V”,方法 int indexOf(int index, char[] arr) 的描述符为“(I[C)I

结合我们的 Class 文件,可以看到该类的第一个方法是构造方法,方法名称为 com.test.BMWCar,描述符为 ()V,也即是一个入参为空且返回值为空的函数

image.png

方法表集合(Methods)

u2             methods_count;//Class 文件的方法的数量
method_info    methods[methods_count];//一个类可以有个多个方法

字段表集合结束之后,紧接着就是方法表集合,与字段表的结构一样,一次包括访问标志(access_flags)、名称索引(name_index)、描述符(descriptor_index)、属性表集合(attributes)几项,具体结构如下表所示:

类型 描述 备注
u2 access_flags 记录方法的访问标志
u2 name_index 常量池中的索引项,指定方法的名称
u2 descriptor_index 常量池中的索引项,指定方法的描述符
u2 attributes_count attributes 包含的项目数
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_VARARGE 0x0080 方法是否接受不定参数
ACC_NATIVE 0x0100 方法是否为 native
ACC_ABSTRACT 0x0400 方法是否为 abstract
ACC_SYNTHETIC 0x0800 方法是否为 strictfp
ACC_SYNTHETIC 0x1000 由编译器产生,不存在于源代码中

与属性表的方法标志进行比较,我们不难发现,两者大体上是类似的,但有诸多不同之处:

因为 volatile 关键字和 transient 关键字不能够修饰方法,所以方法表的访问标志中没有了 ACC_VOLATILE 标志和 ACC_TRANSIENT 标志。与之相对应的,synchronized、native、strictfp 和 abstract 等关键字可以修改方法但不能修饰属性,因此增加了 ACC_SYNCHRONIZEDACC_NATIVEACC_SYNTHETICACC_ABSTRACT

分析到这里,可能会有小伙伴有疑问了:前面说的好像都只是方法的定义,那方法的主体逻辑代码怎么描述呢?

简单来说,方法体中的 Java 代码,经过 Javac 编译成字节码指令之后,存放在方法属性中的一个名为 Code 的属性里面了。

image.png

结合我们的 Class 文件可以看到,该文件中有一个 drive() 方法,该方法的入参和返回值都为空,访问限定符为 public

当然与字段表集合相应的,如果父类方法在子类中被重写(Override),方法表集合中就不会出现来自父类的方法信息。如果未被覆盖就有可能出现由编译器自动添加的方法,最常见的便是类构造器(<clinit>())以及实例构造器 <init>() 方法。

结合我们的 Class 文件可以看到,该 Class 文件也是具有 <init>() 方法的。

属性表集合(Attributes)

   u2             attributes_count;//此类的属性表中的属性数
   attribute_info attributes[attributes_count];//属性表集合

属性表(attribute_info)前边实际上已经提到了数次,Class 文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有信息。

与其他数据项目的要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松一点,不要求各个属性的严格顺序,只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java 虚拟机在运行时会忽略掉它所不熟悉的信息。

由于 Jave 当前支持的属性种类很多已经达到了 29 项,因而此处只列出常见的几种重要属性:

属性名称 使用位置 含义
Code 方法表 Java 代码编译成的字节码指令
ConstantValue 字段表 final 关键字定义的常量值
Deprecated 类、方法表、字段表 被声明为 deprecated 的方法和字段
Exceptions 方法表 方法抛出的异常
InnerClasses 类文件 内部类列表
LineNumberTable Code 属性 Java 源码的行号与字节码指令的对应关系
LocalVariableTable Code 属性 方法的局部变量描述
SourceFile 类文件 源文件名称
Synthetic 类、方法表、字段表 标识方法或字段为编译器自动生成的

Code 属性

Java 方法里的代码被编译处理后,变为字节码指令存储在方法表的 Code 属性里,但并不是所有的方法表里都有 Code 属性,例如接口或抽象类中的方法就可能没有该属性。

Code属性 如下表所示:

类型 名称 含义
u2 attribute_name_index 属性名称索引
u4 attribute_length 属性长度
u2 max_stack 操作数栈深度的最大值
u2 max_locals 局部变量表所需的存储空间
u4 code_length 字节码长度
u1 code[code_length] 存储字节码指令的一系列字节流
u2 exception_table_length 异常表长度
exception_info exception_table 异常表的值
u2 attributes_count 属性数量
attribute_info attributes[attributes_count] 属性的值

结合我们的 Class 文件可以看到其 Code 属性如下:

image.png

从图中可以看出,Code属性 本身也是一个复合属性,其中包含了其他属性,比如包含了 LineNumberTableLocalVariableTable

ConstantValue 属性

只有当一个字段被声明为 static final 时,并且该字段是基本数据类型或 String 类型时,编译器才会在字段的属性表集合中增加一个名为 ConstantValue 的属性,所以 ConstantValue 属性只会出现在字段表中,其数据结构为:

类型 名称 含义
u2 attribute_name_index 属性名称索引
u2 attribute_length 属性长度
u2 constantvalue_index 常量池常量的索引

总结

Class 文件是 Java 虚拟机执行引擎的数据入口,也是 Java 技术体系的基础支柱之一,因而学习 Class 文件的结构很有意义。本文主要讲解了 Class 文件结构中的各个组成部分,以及每个部分的定义、数据结构和使用方法。并结合一个例子(文中有代码,引用处附带有链接),讲解了 Class 文件是如何被存储和访问的。

参考

  1. Java Class 文件结构解析
  2. 类文件结构
  3. 《深入理解 JVM 虚拟机》
  4. 文中使用的 class 文件 BMWCar.class
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3167 引用 • 8207 回帖
  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...