Java之Class类文件结构(学了这么久Java,你了解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

在这里插入图片描述

常量池

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

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

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

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

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

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

在这里插入图片描述

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

访问标志

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

标志名称标志值含义
ACC_PUBLIC0x0001是否为public类型
ACC_FINAL0x0010是否被声明为final
ACC_SUPER0x0020是否允许使用invokespecial字节码指令
ACC_INTERFACE0x0200标识是一个接口
ACC_SYNTHETIC0x1000标识这个类并非由用户代码产生
ACC_ANNOTATION0x2000标识这是一个注解
ACC_ENUM0x4000标识这是一个枚举

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

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

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

对应类查找关系为:

在这里插入图片描述

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

字段表集合

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

  • 字段的作用域(public private …)
  • 是类变量还是实例变量(static)
  • 是否可变(final)
  • 是否并发可见(volatile)
  • 是否可序列化(transient)
  • 数据类型(基本类型、数组、对象)
  • 字段名称

字段表结构:

类型名称数量
u2access_flags1
u2name_index1
u2descriptor_index1
u2attributes_count1
attribute_infoattributesattributes_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类文件内部类列表
LineNumberTableCode属性Java源码的行号与字节码指令的对应关系
LocalVariableTableCode属性方法的局部变量表
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"
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值