查看原文
其他

深入理解JVM - Class类文件的结构

xiaolyuh SpringForAll社区 2021-05-26

点击上方☝SpringForAll社区 轻松关注!

及时获取有趣有料的技术文章

本文来源:http://r6d.cn/bk2hg


Class文件是Java虚拟机执行引擎的数据入口,也是Java技术体系的基础支柱之一。

Class文件本质

Class文件本质上是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。

Class文件格式

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

  • 无符号数:属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
  • 表:表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。

无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时候称这一系列连续的某一类型的数据为某一类型的“集合”。

文件格式

类型名称数量描述
u4magic1魔数(魔法数字),表明当前文件是.class文件,固定0xCAFEBABE
u2minor_version1副版本号
u2major_version1主版本号,从45开始
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各种属性

示例

public class ClassFormat {
    private int m;

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

直接查看Class文件:

我们可以使用javap -verbose ClassFormat直接翻译Class文件,而不用人工翻译,如:

PS E:\> javap -verbose ClassFormat
警告: 二进制文件ClassFormat包含com.xiaolyuh.ClassFormat
Classfile /E:/ClassFormat.class
  Last modified 2020-1-18; size 385 bytes
  MD5 checksum 6fe0cc9a0e7bf5b27718cb7e42ff6bfd
  Compiled from "ClassFormat.java"
public class com.xiaolyuh.ClassFormat
  minor version: 0                        // 次版本号
  major version: 52                       // 主版本号
  flags: ACC_PUBLIC, ACC_SUPER            // 类的访问标志
Constant pool:                            // 常量池
   #1 = Methodref          #4.#18         // java/lang/Object."<init>":()V [从1开始]
   #2 = Fieldref           #3.#19         // com/xiaolyuh/ClassFormat.m:I
   #3 = Class              #20            // com/xiaolyuh/ClassFormat  [类索引(this_class)表示引用20索引位]
   #4 = Class              #21            // java/lang/Object [父类索引(super_class)]
   #5 = Utf8               m              // 成员变量m的简单名称
   #6 = Utf8               I              // 表示int类型
   #7 = Utf8               <init>         // 实例构造器
   #8 = Utf8               ()V            // 没有参数,没有返回值的方法
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/xiaolyuh/ClassFormat;
  #14 = Utf8               inc             // 方法inc的简单名称
  #15 = Utf8               ()I
  #16 = Utf8               SourceFile
  #17 = Utf8               ClassFormat.java
  #18 = NameAndType        #7:#8          // "<init>":()V
  #19 = NameAndType        #5:#6          // m:I
  #20 = Utf8               com/xiaolyuh/ClassFormat
  #21 = Utf8               java/lang/Object
{
  public com.xiaolyuh.ClassFormat();          // 构造函数
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1          // args_size=1的原因是this变量
         0: aload_0                           // 将局部变量槽 0(即this指针)的元素入栈
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return                            // 方法正常返回
      LineNumberTable:
        line 7: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/xiaolyuh/ClassFormat;

  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 11: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       7     0  this   Lcom/xiaolyuh/ClassFormat;
}
SourceFile: "ClassFormat.java"

更多指令请参考深入理解JVM - 虚拟机字节码指令集。

常量池

常量池中的第一个常量有特殊含义,所以常量池计数是从1开是的,其他计数是从0开始,如:接口索引集合。如上图,常量池计数是16,表示有22个常量,排除第一个,表示有21个常量,索引值范围为1~21。

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

  • 字面量:比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。
  • 符号引用:属于编译原理方面的概念,主要包括:类和接口的全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符。

访问标志

用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。

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

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

字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。Java语言中的“字段”(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。

!! 字段可以包括的修饰符有字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。

字段访问标志:

描述符

描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示。对于数组类型,每一维度将使用一个前置的“[”字符来描述,如一个定义为“java.lang.String[][]”类型的二维数组将被记录成“[[Ljava/lang/String;”,一个整型数组“int[]”将被记录成“[I”。

描述符表:java类型 | 类型描述符 ---|--- boolean|Z char |C byte|B short|S int|I long|J float| F double |D Object |Ljava/lang/Object; int[]| [I Object[][]| [[Ljava/lang/Object;

方法描述符:方法|方法描述符 ---|--- void m(int i, float f)|(IF)V int m(Object o)|(Ljava/lang/Object;)I int[] m(int i, String s)|(ILjava/lang/String;)I Object m(int[] i)|([I)Ljava/lang/Object;

方法表集合

描述了方法的定义,但是方法里的Java代码,经过编译器编译成字节码指令后,存放在属性表集合中的方法属性表集合中一个名为“Code”的属性里面。与字段表集合相类似的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“<clinit>”方法和实例构造器“<init>”。

属性表集合

存储Class文件、字段表、方法表都自己的属性表集合,以用于描述某些场景专有的信息。如方法的代码就存储在Code属性表中。

Code属性表的结构:

  • max_stack代表了操作数栈(Operand Stack)深度的最大值。
  • max_locals代表了局部变量表所需的存储空间。max_locals的单位是变量槽(Slot),变量槽是虚拟机为局部变量分配内存所使用的最小单位。对于byte、char、float、int、short、boolean和returnAddress等长度不超过32位的数据类型,每个局部变量占用一个变量槽,而double和long这两种64位的数据类型则需要两个变量槽来存放。
  • code_length和code用来存储Java源程序编译后生成的字节码指令。

参考

《深入理解JAVA虚拟机》


2021Java深入资料领取方式回复“20210112”

墙裂推荐

【深度】互联网技术人的社群,点击了解!





 深入剖析线上内存溢出的原因

 后端线上问题排查常用命令收藏

 深入理解JVM - 类加载机制

 深入理解JVM - ZGC垃圾收集器

 深入理解 Spring 事务原理


关注公众号,回复“spring”有惊喜!!!

如果资源对你有帮助的话

❤️给个在看,是最大的支持❤️

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存