2 java class文件解析


前言

在正式进入JVM结构学习之前我们先了解一下.class文件的结构。因为在对JVM一无所知的情况下,我们只知道java程序想要运行,一定要编译成class文件,我们先抛开为什么要这要设计,最起码我们找到了入口,对一段程序的解读最重要的不就是入口吗,JVM也是一段程序,我们先从了解其数据来源开始吧!


1. Class文件的总体结构

名称占用字节数数量说明
魔数magic4字节1
次版本号minor_version2字节1
主版本号major_version2字节1
常量池大小constant_pool_count2字节1常量池入口,容量计数从1开始,如果这里显示9,那么常量的编号为1-8,共8个常量
常量池constant_poolcp_infoconstant_pool_count - 1常量池存储了该class文件及其子结构引用的各种常量,包括文字字符串、final声明的变量、类名、方法名。常量池是用一个cp_info结构来描述的,cp_info是一个复合结构,我们会详细说明该符合结构
访问方式access_flags2字节1
this_class2字节1
super_class2字节1
interfaces_count2字节1
interfaces2字节interfaces_count
fields_count2字节1
fieldsfield_infofields_count
methods_count2字节1
methodsmethod_infomethods_count
attributes_count2字节1
attributesattribute_infoattributes_count

2. 案例

2.2 源代码

public class Test {
    private String userName;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }
}

使用javac Test.java编译,再使用javap -verbose Test.class反编译

2.3 反编译

Classfile /E:/leetcode/leetcode/editor/cn/Test.class
  Last modified 2022-5-30; size 398 bytes
  MD5 checksum 9c54ba545f52a6bc3b20ac618cd66d82
  Compiled from "Test.java"
public class Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#17         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#18         // Test.userName:Ljava/lang/String;
   #3 = Class              #19            // Test
   #4 = Class              #20            // java/lang/Object
   #5 = Utf8               userName
   #6 = Utf8               Ljava/lang/String;
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               getUserName
  #12 = Utf8               ()Ljava/lang/String;
  #13 = Utf8               setUserName
  #14 = Utf8               (Ljava/lang/String;)V
  #15 = Utf8               SourceFile
  #16 = Utf8               Test.java
  #17 = NameAndType        #7:#8          // "<init>":()V
  #18 = NameAndType        #5:#6          // userName:Ljava/lang/String;
  #19 = Utf8               Test
  #20 = Utf8               java/lang/Object
{
  public Test();
    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 java.lang.String getUserName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field userName:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 5: 0

  public void setUserName(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      #2                  // Field userName:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 9: 0
        line 10: 5
}
SourceFile: "Test.java"

2.3.1 major version

主版本号和java版本对应关系

Java 1.2 uses major version 46.
Java 1.3 uses major version 47.
Java 1.4 uses major version 48.
Java 5 uses major version 49.
Java 6 uses major version 50.
Java 7 uses major version 51.
Java 8 uses major version 52.
Java 9 uses major version 53.

2.3.2 minor version

次版本号

2.3.3 flags

access_flags 描述的是**当前类(或者接口)**的访问修饰符, 如public, private等,
此外, 这里面还存在一个标志位, 标志当前的额这个class描述的是类, 还是接口

表示名标志值标志含义针对的对象
ACC_PUBLIC0x0001public类型所有类型
ACC_FINAL0x0010final类型
ACC_SUPER0x0020使用新的invokespecial语义类和接口
ACC_INTERFACE0x0200接口类型接口
ACC_ABSTRACT0x400抽象类型类和接口
ACC_SYNTHETIC0x1000该类不由用户代码生成所有类型
ACC_ANNOTATION0x2000注解类型注解
ACC_ENUM0x4000枚举类型枚举
  1. ACC_PUBLIC:该class的访问方式为public
  2. ACC_SUPER:ACC_SUPER 标志用于确定该 Class 文件里面的 invokespecial 指令使用的是哪一种执行语义。
    该如何理解使用哪种执行语义呢? 在java1.1之前调用父方法使用的是invokenonvirtual指令,invokenonvirtual不会使用虚函数查找,总是使用静态绑定
    又如何理解总是静态绑定呢? 现有GrandParentParentChild三个类,Parent继承自GrandParent,Child继承自Parent,现GrandParent中有method,Parent中没有重写method,那么Child中调用super.method()一定是指GrandParent的method,这没有任何问题,但是当Parent中重写method后,仅重新编译Parent,Child调用super.method(),仍然调用的GrandParent中的method!因为没有对Child进行编译,此时其使用invokenonvirtual指令调用父方法,已经静态绑定为使用GrandParent的method。
    在java1.1之后调用父方法使用invokespecial,会去寻找最近的超类方法。
    ACC_SUPER就是用来区分调用父方法使用的是invokenonvirtual还是invokespecial。java 1.0.2版本不会设置ACC_SUPER并且遇到ACC_SUPER时会自动忽略,调用父方法使用invokenonvirtual,使用静态绑定;java1.1版本之后总是会设置为ACC_SUPER,以支持一个正确的调用。

2.3.4 常量池⭐

我们这里说的常量池是指静态常量池,存在于class文件当中。

2.3.4.1 什么是常量?

常量是指在程序的整个运行过程中值保持不变的量。

2.3.4.2 常量池范围?
  1. 字面量:类的全限定路径、类名、字段名、方法名、常量字段的字面量、方法入参的全限定路径
  2. 符号引用:Methodref、Fieldref、Class等。
2.3.4.3 常量池中常量种类
  1. 字面量
    1.1 字符串常量:不仅仅包括
    1.2 final常量
    1.3 整数型常量
  2. 符号引用
    2.1 类和接口的全限定名
    2.2 字段的名称和描述符
    2.3 方法的名称和描述符

补充:为什么将符号引用当作常量?
因为在编译阶段,JVM不会保存各个方法和字段的最终内存布局信息,因此需要一个常量临时代表内存布局信息,这就是符号引用。符号引用就是用一组符号来表示要引用的目标,符号可以是任何形式的字面量,只要能够无歧义的定位到目标即可。
只有在将类加载到JVM后进行动态链接,将字段、方法的符号引用经过运行期转换转换为直接引用,才能正常使用。

2.3.4.4 看懂常量池
  1. 字面量
    1.1 字符串常量

    public String s = "abc"; 中的"abc"s都为常量,其中"abc"为字符串常量。

    1.2 被final修改的变量(静态变量、成员变量、局部变量)。因为该“变量”在编译时值就确定了且不能更改!

    public final int i = 257;中的257i和类型int并记录为I都为常量,其中257为字面量。
    应当注意,public int i2 = 233; 中的233并不会被放到常量池,常量池中仅保留其类型int并记录为I,和变量名i2,字面量233并不会保存在常量池。

  2. 符号引用
    2.1 类和接口的全限定名

    Class类型
    #4 = Class #20 // java/lang/Object

    2.2 字段的名称和描述符

    如果是成员变量则类型为Fieldref,记录变量和类型,如果是局部变量,则常量池中仅仅保存字段名,类型为Utf8
    #2 = Fieldref #3.#18 // Test.userName:Ljava/lang/String;

    2.3 方法的名称和描述符

    #11 = Utf8 getUserName // 方法名
    #12 = Utf8 ()Ljava/lang/String; // 参数类型+返回值
    可知getUserName方法的参数为空,返回值为String类型。

2.3.4.5 读懂常量池

我们还是以上述的常量池进行讲解。

Constant pool:
   #1 = Methodref          #4.#17         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#18         // Test.userName:Ljava/lang/String;
   #3 = Class              #19            // Test
   #4 = Class              #20            // java/lang/Object
   #5 = Utf8               userName
   #6 = Utf8               Ljava/lang/String;
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               getUserName
  #12 = Utf8               ()Ljava/lang/String;
  #13 = Utf8               setUserName
  #14 = Utf8               (Ljava/lang/String;)V
  #15 = Utf8               SourceFile
  #16 = Utf8               Test.java
  #17 = NameAndType        #7:#8          // "<init>":()V
  #18 = NameAndType        #5:#6          // userName:Ljava/lang/String;
  #19 = Utf8               Test
  #20 = Utf8               java/lang/Object
  1. methodref:是CONSTANT_Methodref_info类型的简写,表示类的方法引用表类型。该类型的结构如下:
CONSTANT_Methodref_info{
    u1 tag = 10; // 当虚拟机访问到常量池中一个数据项的第一个字节为0x0A,那么就代表该数据项是methodref类型
    u2 class_index; // 指向此方法的所属类
    u2 name_type_index; // 指向此方法的名称和类型
}

#1的方法是java.lang.Object类的名称为<init>的无参数()无返回值V的方法。

  1. Fieldref:是CONSTANT_Fieldref_info类型的简写,表示字段引用表类型。该类型的结构如下:
CONSTANT_Fieldref_info{
    u1 tag = 9; // // 当虚拟机访问到常量池中一个数据项的第一个字节为0x09,那么就代表该数据项是fieldref类型
    u2 class_index; // 指向此字段的所属类
    u2 name_type_index; // 指向此字段的名称和类型
}

#2表示的字段是Test类下的一个名为userName的类型为String类型的字段。

  1. Class是CONSTANT_Class_info类型的简写,表示对类和接口的符号引用,该类型的结构如下:
CONSTANT_Class_info{
    u1 tag = 7; // // 当虚拟机访问到常量池中一个数据项的第一个字节为0x07,那么就代表该数据项是Class类型
    u2 name_index; // 指向此字段的全限定名,一个 CONSTANT_Utf8_info类型的值
}

在这里插入图片描述
#4表示的类是全限定名为java/lang/Object的类。

  1. Utf8是CONSTANT_Utf8_info类型的简写,字符串的字段值、字段名、方法名、方法的参数和返回值类型、全限定类名都是使用该类型表示,该类型的结构如下:
CONSTANT_Utf8_info{
    u1 tag = 1; // // 当虚拟机访问到常量池中一个数据项的第一个字节为0x01,那么就代表该数据项是Utf8类型
    u2 length; // 表示存储的byte数组长度
    u1 bytes[length]; //字节类型存储
}

由于Utf8类型的length是u2类型,占两个字节,所以最多能存储65535个字节。

  1. 我们看到#9#10有两个非常奇怪的字符串,分别为CodeLineNumberTable,这个与方法区有关,我们稍后再看。

2.3.5 方法区⭐

我们可以看到上述类中有三个方法,我们选取一个方法作为案例讲解:

public void setUserName(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      #2                  // Field userName:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 9: 0
        line 10: 5
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

tinpo_123

感谢给小张填杯java~

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

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

打赏作者

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

抵扣说明:

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

余额充值