美文网首页
类文件结构

类文件结构

作者: JBryan | 来源:发表于2020-02-14 14:46 被阅读0次

    虚拟机可以运行在各种不同平台上,这些虚拟机都可以载入和执行同一种与平台无关的字节码,从而实现了程序的“一次编写,到处运行”。各种不同平台的虚拟机与所有平台都统一使用字节码存储程序。
    Java语言中的各种变量,关键字和运算符号的语义最终都是由多条字节码命令组合而成的。

    Class类文件的结构

    Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在Class文件中,中间没有添加任何分隔符。
    整个Class文件本质上是一张表,由下图所示数据项构成。

    Class.jpg
    魔数与Class文件的版本
    每个Class文件的头4个字节成为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。Class文件的魔数值为"0xCAFEBABY"。
    紧接着魔数的4个字节存储的是Class文件的版本号:第5个和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。
    一段简单的Java代码
    package com.ljessie.jvm;
    
    public class TestClass {
        private int m;
        public int inc(){
            return m+1;
        }
    }
    
    

    使用Javac命令编译TestClass.java,生成TestClass.class文件,然后使用vim命令查看TestClass.class文件,解决乱码之后,结果如下图:

    TestClass.jpg
    头四个字节魔数的十六进制表示是:cafe babe,第五和第六个字节次版本号为:00 00,第七和第八个字节主版本号为:00 34,也就是十进制的52。
    常量池
    紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库。由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池数量计数值。
    这个容器计数是从1开始的,在十六进制是13,即十进制的19,即常量池中有18项常量
    TestClass_ConstantPool.jpg
    常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)。字面量比较接近于Java语言层面的常量概念,如字符串和final修饰的常量值等。而符号引用则包含下面三类常量:
    类和接口的全限定名(Fully Qualified Name)
    字段的名称和描述符(Descriptor)
    方法的名称和描述符
    手工解析常量池比较痛苦,我这里直接使用javap -verbose C:\workspace\BioDemo\src\main\java\com\ljessie\jvm\TestClass.class命令,查看TestClass.class文件得到
    Classfile /C:/workspace/BioDemo/src/main/java/com/ljessie/jvm/TestClass.class
      Last modified 2020-2-13; size 291 bytes
      MD5 checksum 4ab0ac51fd79f1b9b4a7745e53d4c434
      Compiled from "TestClass.java"
    public class com.ljessie.jvm.TestClass
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
       #2 = Fieldref           #3.#16         // com/ljessie/jvm/TestClass.m:I
       #3 = Class              #17            // com/ljessie/jvm/TestClass
       #4 = Class              #18            // java/lang/Object
       #5 = Utf8               m
       #6 = Utf8               I
       #7 = Utf8               <init>
       #8 = Utf8               ()V
       #9 = Utf8               Code
      #10 = Utf8               LineNumberTable
      #11 = Utf8               inc
      #12 = Utf8               ()I
      #13 = Utf8               SourceFile
      #14 = Utf8               TestClass.java
      #15 = NameAndType        #7:#8          // "<init>":()V
      #16 = NameAndType        #5:#6          // m:I
      #17 = Utf8               com/ljessie/jvm/TestClass
      #18 = Utf8               java/lang/Object
    {
      public com.ljessie.jvm.TestClass();
        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 3: 0
    
      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 6: 0
    }
    SourceFile: "TestClass.java"
    

    常量池的第一项,指向第4和第15项常量,第4项常量指向第18项常量,即java/lang/Object,第15项常量指向第7和第8项,第7项指向<init>,第8项指向()V。
    访问标志
    在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public型;是否定义为abstract类型;如果是类的话,是否被生命为final等。具体的标志位以及标志的含义见下表

    访问标志.jpg
    access_flags一共有16个标志位可用,当前只定义了其中8个,没有使用到的标志位一律要求为0。TestClass是一个普通Java类,被public修饰,不是final和abstract,因此,ACC_PUBLIC,ACC_SUPER为真,其他6个值为假。因此它的access_flags值应为0x0001|0x0020=0x0021。
    访问标志21.jpg
    即用Javap命令查看TestClass.class文件中的flags
    访问标志flag.jpg
    类索引,父类索引与接口集合
    类索引(this_class)和父类索引(super_class)都是一个u2型数据,接口索引集合是一组u2类型的数据的集合。Class文件中由这三项数据来确定这个类的继承关系。
    类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。
    类索引,父类索引和接口索引集合都按顺序排列在访问标志之后。类索引和父类索引各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。
    类索引.jpg
    类索引为0003,父类索引为0004。类索引0003在常量池中指向第17项常量,第17项常量是:com/ljessie/jvm/TestClass;父类索引0004,指向常量池中第18项常量,第18项常量是:java/lang/Object
    类索引2.jpg
    字段表集合
    字段表(field_info)用于描述接口或者类中声明的变量。字段不包括局部变量。描述一个字段可以包含的信息有:作用域(public,private等),实例变量还是类变量(static),可变性(final),并发可见性(volatile),字段数据类型(int,long等),字段名称。这些信息,各个修饰符都是布尔值,要么有某个修饰符,要么没有。而字段名称和数据类型引用常量池中的常量来描述。下图表示字段表的最终格式。
    字段表结构.jpg
    字段修饰符放在access_flags中,与类中的access_flags类似,都是一个u2型数据,可以设置的标志位和含义如下图。
    字段访问标志.jpg
    ACC_PUBLIC,ACC_PRIVATE,ACC_PROTECTED最多只能选一个,ACC_FINAL,ACC_VOLATILE不能同时选择。接口之中的字段必须有ACC_PUBLIC,ACC_STATIC,ACC_FINAL。
    跟随access_flags标志的是两项索引值:name_index和descriptor_index。他们都是对常量池的引用,代表着字段的简单名称以及字段和方法的描述符。
    全限定名: com/ljessie/jvm/TestClass是这个类的全限定名。
    简单名称:没有类型和参数修饰的方法或者字段名称,TestClass中的inc()方法和m字段的简单名称分别是"inc"和m。
    描述符:描述字段的数据类型,方法的参数列表和返回值。根据描述符规则,基本数据类型和void类型都用一个大写字母表示,而对象类型则用L加对象的全限定名来表示。
    描述符标识.jpg
    对于Test Class.class文件来说,字段表集合如下图:
    字段表class.jpg
    0001表示这个类中只有一个字段表数据;0002是access_flags标志,值为ACC_PRIVATE;0005代表字段的简单名称name_index,代表常量池第五项:m;0006代表字段的描述符descriptor_index,代表常量池第六项:I,描述符I又标识基本类型int。可推断出Test Class中定义的字段为:"private int m;"。
    字段表字段.jpg
    字段表集合中,不会列出从父类继承来的字段。但是在内部类中,会自动添加指向外部类实例的字段。
    方法表集合
    方法表的结构和字段表一样,依次包括访问标志(access_flags),名称索引(name_index),描述符索引(descriptor_index)和属性表集合(attributes)。下图表示方法表结构。
    方法表结构.jpg
    方法表的访问标志中增加了ACC_SYNCHRONIZED,ACC_NATIVE等标志。
    方法访问标志.jpg
    方法里的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为Code的属性里面。
    方法表class.jpg
    在TestClass中,方法表第一个数据0002代表集合中有两个方法(<init>和inc()),第一个方法的访问标志为0001,也就是ACC_PUBLIC;名称索引值为0007,代表常量池第7项<init>;描述符索引为0008,代表常量池第8项()V;属性表计数器0001,表示属性表集合有一项属性;属性名称索引为0009,对应常量池Code,说明此属性是方法的字节码描述。
    方法表常量池.jpg
    如果父类的方法在子类中没有被重写,方法表集合中,就不会出现来自父类的方法信息。但是有可能会出现编译器自动添加的方法,例如<init>。
    属性表集合
    在Class文件,字段表,方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
    虚拟机规范中,预定义的属性如下表:
    预定义属性1.jpg
    预定义属性2.jpg
    预定义属性3.jpg
    对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构是自定义的。属性表结构如下:
    属性表结构.jpg
    1.Code属性
    Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在放发表的属性集合中,但并非所有的方法都必须存在这个属性,譬如接口和抽象类中的方法,就不存在Code属性。
    Code属性表结构.jpg
    Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码和元数据两部分,那么在整个Class文件中,Code属性用于描述代码,所有其他数据都用于描述元数据。
    继续以TestClass.class为例,分析<init>中的Code属性。如下图
    Code属性结构实例.jpg
    0001:操作数栈的最大深度,在方法执行的任意时刻,操作数栈都不会超过这个深度,虚拟机运行的时候,需要根据这个值分配栈帧中的操作栈深度。
    0001:本地变量表容量,代表了局部变量表所需的存储空间。单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。对于byte,char,int,boolean等不超过32位的数据类型,每个局部变量需要1个Slot,double和long这种64位的数据类型,需要两个Slot。Slot可以重用,Javac编译器会根据变量的作用域分配Slot给各个变量使用。
    0005:字节码长度为5。
    然后按照顺序依次读入五个字节码指令(每个指令代表含义,查看虚拟机字节码指令表):
    2A:代表指令为aload_0,含义是将第0个Slot中为reference类型的,本地变量推送到操作数栈顶。
    B7:代表指令为invokespecial,作用是以栈顶的reference类型的数据所指向的对象,作为方法接受者,调用此对象的实例构造器方法,private方法或者父类方法。
    0001:为invokespecial参数,查常量池0001对应的常量为实例构造器<init>方法的符号引用。
    B1:代表指令return,含义是返回此方法,返回值为void。
    属性表集合中还有其他属性,这里就不展开说了。

    相关文章

      网友评论

          本文标题:类文件结构

          本文链接:https://www.haomeiwen.com/subject/uwcxfhtx.html