JVM_类文件结构详解

1、前言

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目按照顺序紧凑地排列在Class文件中,中间没有任何分隔符。

使用命令javac将.java 文件编译为.class文件
使用命令javap输出.class文件的字节码内容

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

无符号数:属于基本的数据类型,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节、8个字节的无符号数。无符号数用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。

表:由多个无符号数或其他表作为数据项构成的复合数据类型。所有表都以_info结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,由下表中的数据项构成。

请添加图片描述

学习类文件结构,就是要明白上表中各个数据项的具体含义。

2、 魔数与Class文件版本

魔数
即上表中的magic,4个字节,在Class文件的开头。
作用是确定该文件是否为一个被虚拟机接受的Class文件。使用魔数而不是后缀名的方式是基于安全的考虑:文件扩展名可以任意更改。
正常的Class文件的魔数为0xCAFEBABE。cafe babe ,咖啡宝贝。

版本号
即上表中的major_version、minor_version:Class文件的第5、6个字节是次版本号minor_version,第7、8个字节是主版本号major_version。
高版本的JDK可以向下兼容,但低版本的JDK不向上兼容,虚拟机会拒绝执行。

3、 常量池

常量池是Class文件中的资源仓库,它是Class文件结构中与其他数据项关联最多的数据类型,也是占用Class文件空间最大的数据项之一。

常量池容器计数值
即上表中的constant_pool_count,2个字节
常量池中的常量的数量是不固定的,所以需要constant_pool_count来表示常量池中常量的数量。
该容器计数从1开始(而不是Java语言习惯中从0开始),比如constant_pool_count的值为16,则说明常量池中有15项常量,索引值范围为1-15。将第0项常量空出来的目的是为了满足后面某些指向常量池的索引值的数据在特定情况下表达“不引用任何一个常量池项”的含义。

常量池
从概念上理解,常量池 = constant_pool[constant_pool_count - 1]。
常量池中主要存放两大类常量:字面量和符号引用。
①字面量:接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。
②符号引用:属于编译原理方面的概念,主要包括:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。Java代码在经过javac命令编译之后,生成的Class文件中并不会保存各个方法、字段的最终内存布局信息,必须等到虚拟机运行时,从常量池中获得对应的符号引用,再通过这些符号引用经过类创建或运行时解析、翻译等才能得到具体的内存地址。

常量池中共有14中类型的常量,每一个常量(项)都有自己的表结构,但这14种表的有一个共同特点:表开始的第一位是一个u1类型的标志位tag,代表这个常量属于哪种常量类型。常量池中的常量类型见下表。

请添加图片描述

4、 访问标志

即Class文件数据项表中的access_flags,用于识别类或者接口层次的访问信息:该是类还是接口,是否为public,是否定义为abstract;如果是类,是否声明为final;该类是否由用户代码产生,是否为注解,是否为枚举等信息。

5、 类索引、父类索引和接口索引

Class文件根据这三项数据来确定这个类的继承关系。

类索引
即Class文件数据项表中的this_class:用于确定这个类的全限定名。
父索引
即Class文件数据项表中的super_class:用于确定这个类的父类的全限定名。由于Java是单继承,只有一个父类。所有类(除了java.lang.Object)都有父类。
接口索引
即Class文件数据项表中的interfaces_count和interfaces。interfaces_count表示接口索引表的容量,如果该值为0,接口索引表不占用任何字节。
其中,类索引和父索引各自指向一个常量池中的类型为CANSTANT_Class_info的常量,通过该常量的索引值再定位到常量池中的CANSTANT_Utf8_info类型的常量表示的全限定名字符串。

6、 字段表集合

从概念上理解,字段表集合 = fields[fields_count],用于描述该Class文件对应的代码中声明的变量:包括类级变量以及实例变量,但不包括方法内声明的局部变量。
对于一个字段,描述信息主要有:作用域(public、private、protected),是实例变量还是类变量(有无static修饰),可变性(final),并发可见性(volatile),可否被序列化(transient),数据类型(基本类型、对象、数组),名称。这些信息中各个修饰符都是布尔值(要么有,要么没有),用标志位表示,而名称、数据类型则引用常量池中的常量来描述。
每一个字段会对应一个字段表,字段表的最终结构如下。

请添加图片描述

access_flag
该字段的修饰符信息:是否public、private、protected、static、final、volatile、transient、enum等。
name_index
该字段的简单名称,是对常量池的常量引用。比如在代码中定义private String name,则name字段的简单名称就是name,但是这个name这个字面量是在常量池中的,name_index存储的是对常量池中该常量项的引用。
descriptor_index
该字段的描述符,描述字段的数据类型。
attributes和attributes
该字段的属性表,见下文。
字段表集合中不会列出从超类或父接口中继承而来的字段,但有可能会列出原本Java代码中不存在的字段,比如在内部类中为了保持对外类的访问性,在编译Class文件的时候会自动添加外部类的实例字段。

7、 方法表集合

从概念上理解,方法表集合 = methods[methods_count]。每一个方法对应一个方法表method_info。方法表与字段表的结构一致,只是具体的信息项不同。

请添加图片描述

其中,访问信息access_flag包括是否public、private、protected、static、final、synchronized、native、abstract、strictfp、是否接受不定参数、是否由编译器自动产生等。
其中,方法描述符descriptor_index中描述了方法的参数列表(数量、类型、顺序)和返回值。

而方法体中的代码,经过编译器编译成字节码指令后,存储在方法属性表attributes[attributes_count]中。

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

8、 属性表集合

在Class文件、字段表、方法表中都可以包含自己的属性表集合,以此描述某些场景专有的信息。

Code属性
Java程序方法体中的代码经过javac编译器编译之后,最终变为字节码指令存储在Code属性表中,即Code属性表是方法表的一部分。但并非所有的方法表都存在该属性,比如接口或者抽象类中的抽象方法就不存在Code属性表。Code属性表的结构如下。

请添加图片描述

attribute_name_index和attribute_length
attribute_name_index表示该属性表的名称,即Code,是指向常量池中类型为CANSTANT_Utf8_info的常量的索引。
attribute_length表示该属性值的长度。

max_stack
代表了操作数栈深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配帧栈中的操作深度。

max_locals
代表了局部变量表所需要的存储空间。max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。对于byte、char、float、int、short、boolean、returnAddress等长度不超过32位的数据类型,每个局部变量占用一个Slot,double和long这两种64位的数据类型则需要两个Slot。方法参数(包括实例方法中的隐藏参数this)、显式异常处理器的参数(try-catch中catch块所定义的异常)、方法体中定义的局部变量都需要使用局部变量表来存放。局部变量表中的Slot可以重用,当代码执行超过一个局部变量的作用域时,这个局部变量所占用的Slot可以被其他局部变量所使用。Javac编译器会根据变量的作用域来分配Slot给各个变量使用,然后计算出max_locals的大小。

code_length和code
code_length代表字节码长度,即字节码指令的个数,也就是code的长度。虽然是u4类型(2^32),但虚拟机规范中明确规定一个方法不允许超过65535条字节码指令,即它只使用了u2的长度,超过这个长度,Javac编译器会拒绝编译。

code中存储的是字节码指令的一系列字节流。对于字节码指令,每个指令都是单字节(u1类型)。当虚拟机读取到code中的一个字节码时,就可以找出对应的这个字节码对应的指令,并且可以知道这个指令后面是否需要跟随参数以及参数应当如何理解。因为字节码指令是用1个字节(8位,2^8=256)来表示,所以一共可以表示256条指令。目前,Java虚拟机规范已经定义了其中约200条编码值对应的指令含义。

exception_table_length和exception_table
显式异常处理表集合,对于Code属性来说并不是必须的,表示的是try-catch中的异常信息描述。

Code属性是Class文件中最重要的一个属性,如果把一个Java程序的信息分为代码和元数据两部分,那么在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。

Exceptions属性
属于方法表中的一部分。作用是列出出方法中可能抛出的受检查异常,也就是方法描述时在throws 关键字后面列出的异常。

LineNumberTable属性
属于Code属性的一部分。用于描述Java源代码行号与字节码行号之间的对应关系,它并不是运行时必须的属性,默认生成,可以在javac命令中使用 -g:none或-g:lines属性取消生成该信息。不生成该信息对程序运行的影响:当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。

LocalVariableTable属性
属于Code属性的一部分。用于描述帧栈中局部变量表中的变量与Java源代码定义的变量之间的关系,不是运行时必须的属性,默认生成,可以在javac命令中使用 -g:none或-g:vars属性取消生成该信息。不生成该信息的影响:当其他人引用该方法时,所有的参数名称都会丢失,IDE会使用类似arg0、arg1等占位符代替原有的参数名,给代码编写带来不便。

ConstantValue属性
属于字段表中的一部分。作用是通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量(类变量)才可以使用该属性。虚拟机对类变量和实例变量的赋值方式和时机有所不同。对于实例变量的赋值是在实例构造器方法中进行的。对于类变量,如果是同时有static和final修饰的基本类型数据或String类型数据,则会生成ConstantValue属性来进行初始化,否则在方法中进行初始化。

9、实例分析

定义一个父类Animal,两个接口Eat、Sleep,一个要分析的Rabbit类。

package constructor;
public class Animal {
    protected String weight;
    
    public String getWeight() {
        return weight;
    }
    public void setWeight(String weight) {
        this.weight = weight;
    }
}
package constructor;
public interface Sleep {
    void sleep();
}
package constructor;
public interface Eat {
    void eat();
}
package constructor;
public class Rabbit extends Animal implements Eat, Sleep{

    private String nickName;
    private int age;

    public static final boolean isCute = true;

    @Override
    public void eat() {
        System.out.println("I eat grass");
    }

    @Override
    public void sleep() {
        System.out.println("I sleep well");
    }

    public String play(int temperature){
        if(temperature > 10){
            return "I want to play outside";
        }else {
            return "I want to stay at home";
        }
    }

    public String getNickName() {
        return nickName;
    }

    public void setNickName(String nickName) {
        this.nickName = nickName;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

使用命令javac *.java编译所有源文件,生成class文件。(如果仅仅单独编译Rabbit.java文件会提示找不到类Sleep、Eat和Animal等)。
使用javap -verbose Rabbit.class命令查看Rabbit.class文件的字节码内容。

Classfile /Users/yue/Documents/workspace/idea/datacenter/src/test/constructor/Rabbit.class
  Last modified 2017-7-9; size 1092 bytes
  MD5 checksum 64e6283bb3c70e9c41fb2f72e09aae13
  Compiled from "Rabbit.java"
public class constructor.Rabbit extends constructor.Animal implements constructor.Eat,constructor.Sleep
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #11.#41        // constructor/Animal."<init>":()V
   #2 = Fieldref           #42.#43        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #44            // I eat grass
   #4 = Methodref          #45.#46        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = String             #47            // I sleep well
   #6 = String             #48            // I want to play outside
   #7 = String             #49            // I want to stay at home
   #8 = Fieldref           #10.#50        // constructor/Rabbit.nickName:Ljava/lang/String;
   #9 = Fieldref           #10.#51        // constructor/Rabbit.age:I
  #10 = Class              #52            // constructor/Rabbit
  #11 = Class              #53            // constructor/Animal
  #12 = Class              #54            // constructor/Eat
  #13 = Class              #55            // constructor/Sleep
  #14 = Utf8               nickName
  #15 = Utf8               Ljava/lang/String;
  #16 = Utf8               age
  #17 = Utf8               I
  #18 = Utf8               isCute
  #19 = Utf8               Z
  #20 = Utf8               ConstantValue
  #21 = Integer            1
  #22 = Utf8               <init>
  #23 = Utf8               ()V
  #24 = Utf8               Code
  #25 = Utf8               LineNumberTable
  #26 = Utf8               eat
  #27 = Utf8               sleep
  #28 = Utf8               play
  #29 = Utf8               (I)Ljava/lang/String;
  #30 = Utf8               StackMapTable
  #31 = Utf8               getNickName
  #32 = Utf8               ()Ljava/lang/String;
  #33 = Utf8               setNickName
  #34 = Utf8               (Ljava/lang/String;)V
  #35 = Utf8               getAge
  #36 = Utf8               ()I
  #37 = Utf8               setAge
  #38 = Utf8               (I)V
  #39 = Utf8               SourceFile
  #40 = Utf8               Rabbit.java
  #41 = NameAndType        #22:#23        // "<init>":()V
  #42 = Class              #56            // java/lang/System
  #43 = NameAndType        #57:#58        // out:Ljava/io/PrintStream;
  #44 = Utf8               I eat grass
  #45 = Class              #59            // java/io/PrintStream
  #46 = NameAndType        #60:#34        // println:(Ljava/lang/String;)V
  #47 = Utf8               I sleep well
  #48 = Utf8               I want to play outside
  #49 = Utf8               I want to stay at home
  #50 = NameAndType        #14:#15        // nickName:Ljava/lang/String;
  #51 = NameAndType        #16:#17        // age:I
  #52 = Utf8               constructor/Rabbit
  #53 = Utf8               constructor/Animal
  #54 = Utf8               constructor/Eat
  #55 = Utf8               constructor/Sleep
  #56 = Utf8               java/lang/System
  #57 = Utf8               out
  #58 = Utf8               Ljava/io/PrintStream;
  #59 = Utf8               java/io/PrintStream
  #60 = Utf8               println
{
  public static final boolean isCute;
    descriptor: Z
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: int 1

  public constructor.Rabbit();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method constructor/Animal."<init>":()V
         4: return
      LineNumberTable:
        line 11: 0

  public void eat();
    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           #3                  // String I eat grass
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 21: 0
        line 22: 8

  public void sleep();
    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           #5                  // String I sleep well
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 26: 0
        line 27: 8

  public java.lang.String play(int);
    descriptor: (I)Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: iload_1
         1: bipush        10
         3: if_icmple     9
         6: ldc           #6                  // String I want to play outside
         8: areturn
         9: ldc           #7                  // String I want to stay at home
        11: areturn
      LineNumberTable:
        line 30: 0
        line 31: 6
        line 33: 9
      StackMapTable: number_of_entries = 1
        frame_type = 9 /* same */

  public java.lang.String getNickName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #8                  // Field nickName:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 38: 0

  public void setNickName(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #8                  // Field nickName:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 42: 0
        line 43: 5

  public int getAge();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #9                  // Field age:I
         4: ireturn
      LineNumberTable:
        line 46: 0

  public void setAge(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #9                  // Field age:I
         5: return
      LineNumberTable:
        line 50: 0
        line 51: 5
}
SourceFile: "Rabbit.java"
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

吹老师个人app编程教学

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

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

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

打赏作者

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

抵扣说明:

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

余额充值