Class文件结构你有了解过吗?

1. Class文件结构

Class文件是一组以8个字节为基础单位的二进制流,紧凑排列无分隔符。当存在需要占用8个字节以上空间的数据项时,则按照高位再迁的方式,分割为8个字节进行存储。
Class文件采用类似于C语言结构体的伪数据结构来存储数据,它有两种数据类型:无符号数

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

Class文件结构
类型名称数量释义
u4magic1魔数,用于判定该文件是否为一个能被JVM接受的Class文件,固定值为0xCAFEBABE
u2minor_version1副版本号
u2major_version1主版本号,与副版本号共同组成Class文件格式版本号。
u2constant_pool_count1常量池计数器
cp_infoconstant_poolconstant_pool_count-1常量池
u2access_flags1访问标识
u2this_class1类索引
u2super_class1父类索引
u2interfaces_count1接口记录器
u2interfacesinterfaces_count接口
u2fields_count1字段表计数器
field_infofieldsfields_count字段表
u2methods_count1方法表计数器
method_infomethodsmethods_count方法表
u2attributes_count1属性表计数器
attribute_infoattributesattributes_count属性表

1.1 magic魔数与Class文件版本

魔数:每个Class文件的前四个字节被称为Magic Number,值为0xCAFEBABE,用于确定这个文件是否是能被JVM接受的Class文件;
Minor Version:第5个及第6个字节为次版本号;
Major Version:第7和第8个字节是主版本号。

编写java文件进行编译:

public class Demo {
    public static void main(String[] args) {
        String numberString = "3";
    }
}

使用javac编译之后获取class文件,通过十六进制编辑器winhex打开Class文件我们可以看到如下信息:
Class文件结构

同我们上面所述一致,前四个字节的十六进制表示为0xCAFEBABE,次版本号为第5-6字节,值为0x0000,主版本号为第7-8字节值为:0x0034,即十进制52,Java的版本号从45开始,从JDK1.1开始,每个JDK的大版本的主版本号在原基础加一(45+7=52,也就是JDK8现在的主版本号)。高版本JDK只可以向下兼容,即使文件格式并未发生变化,超过其版本号范围的JVM拒绝执行。

1.2 常量池

常量池是Class文件结构中与其他项目关联最多的数据,也是Class文件空间最大的数据项之一,它还是Class文件中第一个出现的表类型数据项。

每个Class文件的第9和第10个字节代表常量池容量计数器constant_pool_count
常量池结构
常量池容量计数从1开始(第0项常量之所以空置出来,主要用于表达某些指向常量池的索引需要表达不引用任何一个常量池的项目的含义)。
如上图所示,常量池容量为0x0011即十进制17,说明常量池中有16项常量,索引取值范围1~16。

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

  1. 字面量(Literal):例如文本字符串、被声明为final的常量值、基本数据类型值等。
  2. 符号引用(Symbolic References):主要包含以下几类
    1. 被模块导出或开放的包(Package);
    2. 类和接口的全限定名(Fully Qualified Name);
    3. 字段的名称和描述符(Descriptor);
    4. 方法的名称和描述符;
    5. 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic);
    6. 动态调用点和动态常量(Dynamically-Computed Call Side、Dynamically-Computed Constant)

1.2.1 常量池的项目类型

常量池中每一项常量都是一个表,常量池的项目类型如下:

常量池的项目类型
常量项目类型描述
CONSTANT_Utf8_info(UTF-8编码字符串表)tagu11
lengthu2UTF-8编码的字符串占用的字节数
bytesu1长度为length的UTF-8编码的字符串
CONSTANT_Integer_info(整型常量表)tagu13
bytesu4按照高位在前存储的int值
CONSTANT_Float_info(浮点型常量表)tagu14
bytesu4按照高位在前存储的float值
CONSTANT_Long_info(长整型常量表)tagu15
bytesu8按照高位在前存储的long值
CONSTANT_Double_info(双精度浮点型常量表)tagu16
bytesu8按照高位在前存储的double值
CONSTANT_Class_info(类或接口引用表)tagu17
indexu2指向全限定名常量项的索引
CONSTANT_String_info(字符串常量表)tagu18
indexu2指向字符串字面量的索引
CONSTANT_Fieldref_info(字段引用表)tagu19
class_indexu2指向声明字段的类或接口描述符CONSTANT_Class_info的索引项
name_type_indexu2指向声明字段的类或接口描述符CONSTANT_NameAndType的索引项
CONSTANT_Methodref_info(方法引用表)tagu110
class_indexu2指向声明方法的类描述符CONSTANT_Class_info的索引项
name_type_indexu2指向名称及类型描述符CONSTANT_NameAndType的索引项
CONSTANT_InterfaceMethodref_info(接口方法引用表)tagu111
class_indexu2指向声明方法的接口描述符CONSTANT_Class_info
name_type_indexu2指向名称及类型描述符CONSTANT_NameAndType的索引项
CONSTANT_NameAndType_info(字段或方法名称类型表)tagu112
class_indexu2指向该字段或方法名称常量项的索引
name_type_indexu2指向该字段或方法描述符常量项的索引
CONSTANT_MethodHandle_info(方法句柄表)tagu115
reference_kindu1值必须在1-9的范围内,他决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为
reference_indexu2值必须是对常量池的有效索引
CONSTANT_MethodType_info(方法类型表)tagu116
descriptor_indexu2值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符
CONSTANT_InvokeDynamic_info(动态方法调用表)tagu118
bootstrap_method_attr_indexu2值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引
name_and_type_indexu2值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符

我们继续查看Class字节码文件:
字节码文件
0x0A转换为十进制是10,结合上面常量池类型表中tag等于10的是CONSTANT_Methodref_info,表中tag字段后是数据类型为u2class_indexname_type_index,结合上面截图,分别对应0x0004(十进制4,指向常量池中第四个常量)和0x000D(十进制13,指向常量池第13个常量)。

通过winhex工具查看class文件的方式过于繁琐,我们还有一种较为直观的查看方式。

1.2.2 javap

F:\>javap -v Demo.class
Classfile /F:/Demo.class
  Last modified 2020-12-25; size 267 bytes
  MD5 checksum ed2eccdd0487686a954e6c2ea894d2a7
  Compiled from "Demo.java"
public class Demo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#13         // java/lang/Object."<init>":()V
   #2 = String             #14            // 3
   #3 = Class              #15            // Demo
   #4 = Class              #16            // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               main
  #10 = Utf8               ([Ljava/lang/String;)V
  #11 = Utf8               SourceFile
  #12 = Utf8               Demo.java
  #13 = NameAndType        #5:#6          // "<init>":()V
  #14 = Utf8               3
  #15 = Utf8               Demo
  #16 = Utf8               java/lang/Object
{
  public Demo();
    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 1: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: ldc           #2                  // String 3
         2: astore_1
         3: return
      LineNumberTable:
        line 3: 0
        line 4: 3
}
SourceFile: "Demo.java"

从如上打印信息可以看到,第一项**#1 = Methodref #4.#13**和我们计算的结果完全一致。

1.3 访问标志

在常量池接着的2个字节代表访问标志(access_flag),这个标志用于表示一些类或接口层次的访问信息。

主要包含以下几点:

  1. Class是类还是接口;
  2. 是否定义为public类型;
  3. 是定义为abstract类型;
  4. 若是类是否被声明为final;
  5. 其他。
访问标志
标志名称标志值含义
ACC_PUBLIC0x0001是否为public
ACC_FINAL0x0010是否被声明为final
ACC_SUPER0x0020是否允许使用invokespecial字节码指令的新语义,JDK1.0.2之后编译出来的类的标志都为true,invokespecial(调用无须动态绑定的实例方法)只能调用:方法;private方法;super.method(),这三类方法的调用在对象在编译时就可以确定
ACC_INTERFACE0x0200标识这是一个接口
ACC_ABSTRACT0x0400是否为abstract类型,对于接口或抽象类,标志位为true
ACC_SYNTHETIC0x1000标识这个类并非由用户代码产生的
ACC_ANNOTATION0x2000标识是注解
ACC_ENUM0x4000标识是枚举
ACC_MODUEL0x8000标识是模块
acc_flag共有16个标志位可用,当前只定义了9个(JDK 9新增了一个),没有使用到的为0x0000。

因Demo.java非final非abstract,public修饰,所以他的access_flags=0x0001+0x0020=0x0021
acc_flag

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

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三类数据来确认该类型的继承关系。类索引用于确定类的全限定名,父类索引引用于确定类的父类全限定名。Java不允许多重继承,父类索引只有一个,除java.lang.Object之外,所有的Java类都有父类,因此java类的父类索引都不为0(java.lang.Object的父类索引为0)。接口索引集合用来描述类实现了哪些接口,这些被实现的接口将按implements关键字后的接口顺序从左到右排列在接口索引集合中。

类索引、父类索引、接口索引集合
在访问标志后的3个u2类型的值分别是0x0003(类索引),0x0004(父类索引),0x0000(接口索引集合),分别代表:类索引为3,父类索引为4,接口集合大小为0。通过我们的javap -v计算出来的常量池找到常量池中索引为3和4的表:

常量池

1.5 字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。字段包括类级变量和实例级变量,不包括方法内部声明的局部变量。

字段可以包括:

  1. 修饰符由字段的作用域(public、private、protected修饰符);
  2. 实例变量还是类变量(static)、可变性(final);
  3. 并发可见性(volatile);
  4. 是否被序列化(transient);
  5. 字段数据类型(基本类型、对象、数组);
  6. 字段名称。

各修饰符都是bool值,字段名称引用常量池中的常量来描述。

字段表结构
类型名称数量
u2access_flags1
u2name_index(字段简单名称)1
u2descriptor_index(描述符)1
u2attributes_count1
attribute_infoattributesattributes_count

access_flags可以设置的标志位及含义如下:

字段访问标志
标志名称标志值含义
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
根据java语法约束,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志只可选其一,ACC_FINAL、ACC_VOLATILE同样也是,接口中,字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL。

简单名称、描述符、全限定名的含义:

  1. 简单名称:没有类型和参数修饰的方法或字段名称。
  2. 全限定名:把类全名中的“.”替换成“/”,为了使连续的多个全限定名之间不产生混淆。
  3. 描述符:用来描述字段的数据类型、方法的参数列表(数量、类型、顺序)和返回值。基本数据类型和代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示。
描述符标识字符含义
标识字符含义
Bbyte
Cchar
Ddouble
Ffloat
Iint
Jlong
Sshort
Zboolean
Vvoid
L对象类型

字段表结构
我们可以看到fields_count为0x0001,access_flags为0x0002,name_index_0x0005,descriptor_index为0x0006,attrbutes_count为0x0000。
javap

1.6 方法表集合

和字段表相似,但访问标志和属性表集合的可选项中有些差异。

方法表结构
类型名称数量
u2access_flags1
u2name_index(字段简单名称)1
u2descriptor_index(描述符)1
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_VARARGES0x0080方法是否接受不定参数
ACC_NATIVE0x0100方法是否native
ACC_ABSTRACT0x0400方法是否abstract
ACC_STRICT0x0800方法是否strictfp
ACC_SYNTHETIC0x1000方法是否有编译器自动产生

方法表结构
如上图所示,方法表容量(methods_count)为0x0002,访问标志位(access_flags):0x0001,名称索引(name_index):0x0007,描述符(descriptor_index):0x0008,属性表计数器(attributes_count):0x0001,属性名称索引(attribute_name_index):0x0009。

在这里插入图片描述

1.7 属性表集合

虚拟机规范预定义属性
属性名称使用位置含义
Code方法表Java代码编译成的字节码指令
ConstantValue字段表final关键字定义的常量值
Deprecated类、方法表、字段表被声明为deprecated的方法和字段
Exceptions方法表方法声明的异常
EnclosingMethod类文件仅当一个类作为局部类或匿名类时才拥有这个属性,这个属性用于标示这个类所在的外围方法
InnnerClass类文件内部类列表
LineNumberTableCode属性Java源码的行号与字节码指令的对应关系
LocalVariableTableCode属性方法的局部变量描述
StackMapTableCode属性JDK6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配
Signature类中、方法表中、字段表中用于支持范型情况下的方法签名,在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。由于Java的泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息
SourceFile记录源文件名称
SourceDebugExtensionJDK1.6中新增的属性,SourceDebugExtension用于存储额外的调试信息。
Synthetic类、方法表、字段表标识方法或字段为编译器自动产生的
LocalVariableTypeTable它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
RuntimeVisibleAnnotations类、方法表、字段表为动态注解提供支持,用于指明哪些注解是运行时(实际上运行时就是进行反射调用)可见的。
RuntimeInvisibleAnnotations类、方法表、字段表与RuntimeVisibleAnnotations相反用于指明哪些注解是运行时不可见的。
RuntimeVisibleParameterAnnotations方法表与RuntimeVisibleAnnotations类似,作用对象为方法的参数。
RuntimeInvisibleParameterAnnotations方法表作用与RuntimeInvisibleAnnotations类似,作用对象为方法的参数。
AnnotationDefault方法表用于记录注解类元素的默认值
BootstrapMethodsJDK7新增的属性,用于保存invokedynamic指令引用的引导方法限定符
RuntimeVisibleTypeAnnotations类、方法表、字段表、Code属性JDK 8新增的属性,用于指明哪些类注解是运行时可见的
RuntimeInvisibleTypeAnnotations类、方法表、字段表、Code属性JDK 8新增的属性,用于指明哪些类注解是运行时不可见的
MethodParameters方法表JDK 8新增的属性,用于支持将方法名称编译进Class文件中,并可运行时获取。
属性表结构
类型名称数量
u2attribute_name_index1
u4attribute_length1
u1infoattribute_length

参考

《深入理Java虚拟机》周志明

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

人生逆旅我亦行人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值