Class类文件结构

由于本部分内容概念性知识过多显的过于繁琐,已经尽力精简,且有些细节仍未写到,所以最后以一个反编译文件为例进行类文件结构分析。


文章目录

  • Class类文件结构
  • 一、Class文件结构
  • 魔数与Class文件的版本
  • 常量池
  • 访问标志
  • 类索引、父类索引与接口索引集合
  • 字段表集合
  • 方法表集合
  • 属性表集合


一、Class文件结构

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔符。如果有效文件不是8字节的整数倍则需要补齐为8字节整数倍

Class文件采用一种类似于C语言结构体的伪结构来存储。这种伪结构只有两种数据类型:无符号数和表

  • 无符号数:以u1 u2 u4 u8来代表1个字节、2个字节、4个字节、8个字节,可以用来描述数字、索引引用、数量值或者根据UTF8编码构成字符串值
  • 表:由多个无符号数或其他表作为数据项构成的复合数据类型,所有表都习惯性以"_info"结尾。

魔数与Class文件的版本

每个Class文件的头4个字节称为魔数,它的唯一作用是用于确定这个文件是否是一个虚拟机可以接受的Class文件,因为只针对后缀名的校验并不安全,可以被随意修改,所以在文件头部增加一个4字节的校验码。

紧接着魔数的4个字节就是存储Class文件的版本号,前两个字节是次版本号(Minor Version),后两个字节是主版本号(Major Version)

Tip:Java的版本号从45开始,JAVA8的主版本号就是52,16进制34换算就是十进制52

java文件结构存储 java类文件结构_jdk

常量池

紧接着主版本号之后的就是常量池的入口,常量池是Class文件结构中与其他项目关联最多的数据类型,也是占用空间最大的数据项目之一,同时也是Class文件中第一个出现的表类型数据项目。

由于常量池的数量不固定,与我们写的程序代码有关,所以在常量池入口需要放置一项u2类型的数据,代表常量池数量(计数从1开始)0项常量空出来表示“不引用任何一个常量池项目”。

常量池中主要存放两大类常量:字面量和符号引用。

  • 字面量:接近于Java语言层面的常量概念,如String文本字符串、被声明为final的常量值等
  • 符号引用:类和接口的全限定名、字段的名称和描述符(public private …)、方法的名称和描述符

Java在进行javac编译时,不像C和C++那样有“连接”步骤,而是在虚拟机加载Class文件时进行动态连接。也就是说Class文件中不保存各个方法和字段的最终内存布局信息,只保存了变量和方法的符号信息,在运行时可以进行动态内存分配加载使用。正是由于这种特性,Java才有了动态扩展的特性。

java文件结构存储 java类文件结构_jdk_02


常量池内的每一项常量都是一个表,共有11中结构各不相同的表结构数据。这11中表都有一个共同的特点,表开始的第一位是一个u1类型的标志位(tag,取值为1-12,2除外)用来表示当前常量的类型。

java文件结构存储 java类文件结构_java文件结构存储_03

常量池大小代表其中有19个常量,之后根据tag可以确定每一项常量所占用的长度就可以知道总共常量池的大小。(根据tag也可以对应查找该类型的常量存储格式,篇幅原因,在这里不在展示分析)

访问标志

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

标志名称

标志值

含义

ACC_PUBLIC

0x0001

是否为public类型

ACC_FINAL

0x0010

是否被声明为final

ACC_SUPER

0x0020

是否允许使用invokespecial字节码指令

ACC_INTERFACE

0x0200

标识是一个接口

ACC_SYNTHETIC

0x1000

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

ACC_ANNOTATION

0x2000

标识这是一个注解

ACC_ENUM

0x4000

标识这是一个枚举

注:invokespecial只能调用三类方法:<init>方法;private方法;super.method()

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

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合(对应JAVA单继承和接口的多实现)。Class文件由这三项数据来确定一个类的继承关系。

对应类查找关系为:

java文件结构存储 java类文件结构_java_04

由类索引到常量池中寻找Class信息,又由Class信息找到存储类的全限定名的UTF8字符串。(父类索引、接口索引均类似)

字段表集合

字段表(field_info)用于描述接口或类中声明的变量,不包括方法内部声明的变量,描述的方面可以有:

  • 字段的作用域(public private …)
  • 是类变量还是实例变量(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描述

name_index:去常量池中寻找字段的简单名称

descriptor_index:去常量池中寻找类型信息找的是描述符

attributes为额外信息(详见属性表集合部分),final常量初始化时attributes里就会存一个ConstantValue的属性为常量赋初始值

tip1:简单名称、描述符、全限定名概念:

全限定名:就是类的全限定名,多个全限定名不产生混淆时,最后会加一个;表示结束

简单名称:则就是没有类型和参数修饰的方法或字段名称

描述符:描述字段的数据类型、方法参数列表和返回值基本类型基本都是首字母大写(byte与boolean首字母重复,所以boolean的描述符为Z)void描述符为V、对象描述符为L、一维数组为[全限定名;、二维数组为[[全限定名;

tip2:在Java语言中字段重载的区别方法不包括返回值,但虚拟机层面可以接受返回值不同的方法重载

方法表集合

方法表的描述与字段表一样,依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。

这些仅用来描述方法的信息,具体方法内部的代码放在属性表集合中一个名为Code的属性里面。

如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息。但是可能出现由编译器自动添加的方法,例类构造器方法<clinit>方法和实例构造器<init>方法

属性表集合

属性表(attribute_info)集合在之前的表集合中都出现过。与Class文件中其他的数据项目要求严格的顺序、长度和长度不同,属性表不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以想属性表中写入自己定义的属性信息,Java虚拟机会忽略掉它不认识的属性。为了能正确地解析Class文件,Java虚拟机规范预定义了9项虚拟机实现应当能识别的属性。

属性名称

使用位置

含义

Code

方法表

Java方法代码编译生成的字节码指令

ConstantValue

字段表

final关键字定义的常量值

Deprecated

类、方法表、字段表

可序列化

Exceptions

方法表

方法抛出的异常

InnerClasses

类文件

内部类列表

LineNumberTable

Code属性

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

LocalVariableTable

Code属性

方法的局部变量表

SourceFile

类文件

源文件名称

Synthetic

类、方法表、字段表

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

Code属性表:

code表的重要属性:

  • max_stack:代表了操作数栈深度的最大值
  • max_locals:代表了局部变量表的大小,max_locals的单位为Slot(虚拟机为局部变量分配内存使用的最小单位),对于byte、char等长度不超过32位的数据类型,每个变量占用1个Slot,而double、long这种数据类型需要两个slot存放。方法参数(包括隐藏参数this)、异常处理器的参数、方法体内部定义的局部变量都需要存放在局部变量表中。局部变量表可以复用,当一个变量超出某个变量的作用域后该变量所占用的Slot就可以被复用
  • exception_table:异常表,具体后面的例子会说

十六进制的字节码文件知道怎么看就可以,必要时可以参考JAVA虚拟机规范的结构表对照着可以看懂就行,重点是反编译后的文件如何看,下面以一个样例程序查看:

package JVMTest;

public class Test {
    private int value1;
    public static int value2;
    public static final int value3 = 123;
    public static final Object obj = new Object();
    public static void main(String[] args) {
        try {
            System.out.println("Hello World");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            System.out.println("finally-------");
        }
    }
    public void test(){
        System.out.println("Test Method");
    }
}

本例包括普通变量、静态变量、静态常量、引用类型常量、静态方法、普通方法、异常代码块,比较全面,我们来反编译下看看源码

//类文件位置
Classfile /D:/people/JavaSourceLearn/src/JVMTest/Test.class
    //最后修改时间和文件大小
  Last modified 2020-6-3; size 913 bytes
    //MD5唯一序列号
  MD5 checksum b32b11fbe2bb030697c4cd3da6174a0a
    //编译自哪个文件
  Compiled from "Test.java"
    //类信息:public JVMTest包下的Test类
public class JVMTest.Test
    //次版本号
  minor version: 0
    //主版本号,52代表JAVA8
  major version: 52
    //访问标志位public类型 super允许使用invokespecial指令
  flags: ACC_PUBLIC, ACC_SUPER
    //常量池内容
Constant pool:
   #1 = Methodref          #9.#33         // java/lang/Object."<init>":()V
   #2 = Fieldref           #34.#35        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #36            // Hello World
   #4 = Methodref          #37.#38        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = String             #39            // finally-------
   #6 = Class              #40            // java/lang/Exception
   #7 = Methodref          #6.#41         // java/lang/Exception.printStackTrace:()V
   #8 = String             #42            // Test Method
   #9 = Class              #43            // java/lang/Object
  #10 = Fieldref           #11.#44        // JVMTest/Test.obj:Ljava/lang/Object;
  #11 = Class              #45            // JVMTest/Test
  #12 = Utf8               value1
  #13 = Utf8               I
  #14 = Utf8               value2
  #15 = Utf8               value3
  #16 = Utf8               ConstantValue
  #17 = Integer            123
  #18 = Utf8               obj
  #19 = Utf8               Ljava/lang/Object;
  #20 = Utf8               <init>
  #21 = Utf8               ()V
  #22 = Utf8               Code
  #23 = Utf8               LineNumberTable
  #24 = Utf8               main
  #25 = Utf8               ([Ljava/lang/String;)V
  #26 = Utf8               StackMapTable
  #27 = Class              #40            // java/lang/Exception
  #28 = Class              #46            // java/lang/Throwable
  #29 = Utf8               test
  #30 = Utf8               <clinit>
  #31 = Utf8               SourceFile
  #32 = Utf8               Test.java
  #33 = NameAndType        #20:#21        // "<init>":()V
  #34 = Class              #47            // java/lang/System
  #35 = NameAndType        #48:#49        // out:Ljava/io/PrintStream;
  #36 = Utf8               Hello World
  #37 = Class              #50            // java/io/PrintStream
  #38 = NameAndType        #51:#52        // println:(Ljava/lang/String;)V
  #39 = Utf8               finally-------
  #40 = Utf8               java/lang/Exception
  #41 = NameAndType        #53:#21        // printStackTrace:()V
  #42 = Utf8               Test Method
  #43 = Utf8               java/lang/Object
  #44 = NameAndType        #18:#19        // obj:Ljava/lang/Object;
  #45 = Utf8               JVMTest/Test
  #46 = Utf8               java/lang/Throwable
  #47 = Utf8               java/lang/System
  #48 = Utf8               out
  #49 = Utf8               Ljava/io/PrintStream;
  #50 = Utf8               java/io/PrintStream
  #51 = Utf8               println
  #52 = Utf8               (Ljava/lang/String;)V
  #53 = Utf8               printStackTrace
  //类中具体代码内容
{
    //变量value2
  public static int value2;
    //I表示是int类型
    descriptor: I
    //表示是public和static类型的
    flags: ACC_PUBLIC, ACC_STATIC
	//value3常量
  public static final int value3;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    //初始化,ConstantValue属性初始化为123
    ConstantValue: int 123
	//obj 引用类型常量,可以看出没有ConstantValue初始化属性
  public static final java.lang.Object obj;
    descriptor: Ljava/lang/Object;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
	//执行类的构造方法
  public JVMTest.Test();
    //()表示无参数、V表示返回值类型为void
    descriptor: ()V
    flags: ACC_PUBLIC
    //code表示方法体内部信息
    Code:
    //stack:操作数栈深度、locals局部变量表大小、args_size参数个数(默认有一个参数this)
      stack=1, locals=1, args_size=1
          //将局部变量表的0号位置读入操作数栈
         0: aload_0
          //执行<init>,实例构造方法
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
          //结束,return 空
         4: return
          //Java代码行数与字节码指令行号的对应关系
      LineNumberTable:
        line 3: 0
	//执行main方法
  public static void main(java.lang.String[]);
    //参数为[全限定名;是String类型的一维数组,返回值为void
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      //args_size为1表示有一个参数,因为是static类型,所有没有this参数
      stack=2, locals=3, args_size=1
         //获取一个静态变量out,类型为L全限定名;PrintStream对象类型
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         //从运行时常量池中获取Hello World字符串
         3: ldc           #3                  // String Hello World
         //调用println方法
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         //无异常执行final方法
         8: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        11: ldc           #5                  // String finally-------
        13: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         //goto跳转到46行return 结束
        16: goto          46
         //如果有异常进入catch方法,Exception类型的e变量放入局部变量表后再获取局部变量表1位置中的e变量
        19: astore_1
        20: aload_1
         //执行
        21: invokevirtual #7                  // Method java/lang/Exception.printStackTrace:()V
         //有异常捕获成功执行final方法
        24: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        27: ldc           #5                  // String finally-------
        29: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        32: goto          46
         //如果出现无法捕获的异常,把catch any放入slot的2号位置(隐藏槽位)
        35: astore_2
        36: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        39: ldc           #5                  // String finally-------
        41: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         //读取slot2号位置的变量any
        44: aload_2
         //抛出异常
        45: athrow
        46: return
         //异常信息表
      Exception table:
         //from 监控起始行号、to监控结束行号,[from,to)、target捕获后跳转行数、捕获类型
         from    to  target type
             0     8    19   Class java/lang/Exception
             0     8    35   any
            19    24    35   any
      LineNumberTable:
        line 10: 0
        line 14: 8
        line 15: 16
        line 11: 19
        line 12: 20
        line 14: 24
        line 15: 32
        line 14: 35
        line 15: 44
        line 16: 46
       //局部变量表
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           20       4     1     e   Ljava/lang/Exception;
            0      47     0  args   [Ljava/lang/String;
       //栈图,为加快Class文件的校验速度,把类型校验时需要用到的相关信息直接写入Class文件中
      StackMapTable: number_of_entries = 3
        frame_type = 83 /* same_locals_1_stack_item */
          stack = [ class java/lang/Exception ]
        frame_type = 79 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
        frame_type = 10 /* same */

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #8                  // String Test Method
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 18: 0
        line 19: 8
	  LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   LJVMTest/Test;
	//静态代码块与静态变量的初始化
  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: new           #9                  // class java/lang/Object
         3: dup
         //调用Object的<init>方法
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
         //让obj引用引用Object对象
         7: putstatic     #10                 // Field obj:Ljava/lang/Object;
        10: return
      LineNumberTable:
        line 7: 0
}
//源码文件
SourceFile: "Test.java"