三、class类文件结构

354 阅读12分钟

Class文件来龙去脉

java之所以能够跨平台,是因为 jdk可以生成可以在各个平台的JVM上运行的中间代码产物 ---.class文件。class文件解除了JVM和java语言之间的耦合。

JVM被设计出来,不仅仅是为了运行java程序,还有 Ruby,Groovy(Gradle中常用),scala(大数据常用)

2023-09-05-10-14-14-image.png

Ruby,groovy,scala经过各自编译过程之后,都会生成class文件,然后统一让JVM运行。

class文件结构

基本构成

从整体来看class文件,它内部只有两种东西:无符号数.

  • 无符号数

    属于基本数据类型,以u1,u2,u4,u8分别代表 1,2,4,8个字节的无符号数,无符号数可以用来表示 数字索引引用数量值 或者 UTF编码的字符串.

  • 表示 由多个无符号数 或者 表 构成的复合数据结构

    class文件中所有的表都以 _info作为后缀

    整个class文件本质上就是一张大表

2023-09-05-10-20-26-image.png

形象理解

以生物化学来类比,一个class文件作为一个完整生命体,它其中由 C,H,OMN等基本的化学元素构成,而 C,H,O这些基本化学元素又可以先构成 生命体的各个组织,器官, 组织和奇怪再构成完整生命体的各个部分。

基本的化学元素其实就是 无符号数,

完整构成

  无符号数和表,按照预先设定好的顺序 紧密排序,相邻结构之间没有间隙。

JVM则是按照这个顺序来解析class文件。

2023-09-05-10-26-48-image.png 每个部分的含义如下:

2023-09-05-10-29-39-image.png

2023-09-05-10-31-18-image.png

案例

我们编辑一个 Main.java文件:

import java.io.Serializable;

public class Main implements Serializable, Cloneable {

    private int num = 1;

    public int add(int i) {
        int j = 10;
        num += i;
        return num;

    }

}

当它编译成class之后,使用十六进制编辑器查看class的内容(先下载winhex):

2023-09-05-10-50-03-image.png

这些数字看上去毫无规律,但是在JVM眼中,他们是经过了严格排序的。

魔数

占用4个字节,也就是上图中的CA FE BA BE ,这是一组固定的数字,它是判断当前文件是否是 class文件的身份标志,如果开头判断就不是class,那么JVM就不会将它视作class文件去执行。

版本号

分为副、主版本号,分别占2个字节,也就是上图中的。

  • 副版本号:00 00 ,对应十进制的0 ,也就是minor_version 是0
  • 主版本号:00 3E,对应十进制的 62,major_version是 62,

也就是说,主版本号62,副版本号0,所以完整版本号是 62.0, 它代表了 Java SE 8(JDK 1.8)所对应的class文件格式。

常量池 (重点)

魔数和版本号中两个部分都是无符号数组成的,而 紧跟在版本号之后的是 常量池表,它是一个表(无符号数组成的复杂结构)。

它的作用是,保存类的各种信息

类的名称,父类的名称,类中的方法名,参数名称,参数类型等。

常量池中又包含很多的表,如下:

2023-09-05-11-17-19-image.png

举例 CONSTANT_class_info 类信息表

它的表结构为:

2023-09-05-11-23-41-image.png

tag是区分于其他表的标志位,7 就表示 是classInfo表,u1表示占用一个字节。

name_index是一个指针,如果 值为2,那么就是指向常量池中第2个常量。

举例 CONSTANT_utf8_info

2023-09-05-11-26-33-image.png

tag值为1,表示 它是utf8类型的表

length 数组长度,如果值为5,那么 接下来就是 5个连续的u1(单字节)类型的数据。

bytes, 数组内容,因为上面length是5,那么这个数组的长度就是5。

面试题 Java字符串的最大长度是多少

其实这个问题存在歧义。需要分解问题来回答:

如果是 在java代码中声明一个字符串常量

如果提问的重点是 在java代码中声明一个字符串常量,我最大可以用多大长度,那么答案如下:

我们在 java代码中声明的字符串,最终存储在class文件的CONSTANT_utf8_info表中,

因此一个字符串所能使用的最大字节数是也就是u2所能代表的最大值 16进制FF FF(对应十进制的65536),但是由于需要两个字节来保存null值,所以 class文件中定义字符串常量的最大长度是 2^16-2 = 65534

如果是 java代码在运行时,允许传递的字符串最大长度

如果提问的重点是 java代码在运行时,允许传递的字符串最大长度是多少

程序运行时, 如果是方法调用之间传字符串类型的实参,那么这个长度就收到JVM和当前可用内存的限制,理论上可用的最大长度是2 ^ 31 . 之所以是 2^31,是因为字符串String实际上是存储在 Char[] 数据结构中的,这是一个数组,字符数组的最大长度受到 整形数组的最大长度限制,而这个长度限制就是Integer.MAX_VALUE,也就是 (2^31).

上面说的是理论值,但是如果当前内存已经不足以分配 2^31个字节数的空间, 那么你如果仍然申请这么大的空间,就会报出 OutOfMemoryError 内存溢出的错误。

表之间相互引用

如果类信息表 class_info 中,有一个字符串类型的常量,那么就有可能 name_index指向 某个utf8_info表。他们之间存在引用关系。

2023-09-05-16-41-42-image.png

常量计数器

开发者在定义一个class的时候,所定义的常量数不尽相同,但是在一个类写完了之后,常量的数目是确定的。所有,在 字节码中,常量池部分的开头,就是 容量计数器,用u2长度(2位的16进制数)表示 常量池中一共有多少个 常量。

2023-09-05-16-48-44-image.png

这里的00 1716进制数,换算成10进制是,23.

但是由于下标为0的常量被JVM预留为其他用途,所以实际的常量数目是22.

常量解析过程

常量计数器之后,紧跟的就是 一个个具体的常量了,

16进制的 0A转化为10进制为10,那么这个表就是 methodref_info 方法引用表,它的结构为:

CONSTANT_Mehtodref_info{
    u1 tag = 10;
    u2 class_index;  // 指向此方法所属类
    u2 name_type_index; // 指向此方法的名称和类型
}

这里表示,用u1表示 表类型,u2 的长度的无符号数 指向 次方法所属类的index,u1长度的无符号数 指向方法的名称和类型。

00 02 -> class_index 表示指向常量池中第2个常量

00 03 -> name_type_index 表示指向此方法的名称和类型,指向常量池中第3个常量

继续往后解析过程也是类似的。但是我们手工去解析是比较费劲的,实际上,JDK提供了javap命令来帮我们解析class文件结构。

javap

将这个class文件用 命令 javap -v Main.class 解析的结果如下:

可以看出:

  • manor version 和 major version 分别为 00 和 62

  • 常量池 (Constant poll)中,显示了常量的顺序以及相互的引用关系

    • 排在第一的是 Methodref, 它引用了#2和#3
    • 第二和第三的是 Class和NameAndType ,他们又分别引用了 #4 和 (#5#6)
Classfile /C:/Users/zwx1245985/Desktop/JAVA/Main.class
  Last modified 202395日; size 351 bytes
  SHA-256 checksum 042459bcab65b80c6bfcb9506a08a242f61034fafe932b8a406e02d13a515f47
  Compiled from "Main.java"
public class Main implements java.io.Serializable,java.lang.Cloneable
  minor version: 0
  major version: 62
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #8                          // Main
  super_class: #2                         // java/lang/Object
  interfaces: 2, fields: 1, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Fieldref           #8.#9          // Main.num:I
   #8 = Class              #10            // Main
   #9 = NameAndType        #11:#12        // num:I
  #10 = Utf8               Main
  #11 = Utf8               num
  #12 = Utf8               I
  #13 = Class              #14            // java/io/Serializable
  #14 = Utf8               java/io/Serializable
  #15 = Class              #16            // java/lang/Cloneable
  #16 = Utf8               java/lang/Cloneable
  #17 = Utf8               Code
  #18 = Utf8               LineNumberTable
  #19 = Utf8               add
  #20 = Utf8               (I)I
  #21 = Utf8               SourceFile
  #22 = Utf8               Main.java
{
  public Main();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: iconst_1
         6: putfield      #7                  // Field num:I
         9: return
      LineNumberTable:
        line 3: 0
        line 5: 4

  public int add(int);
    descriptor: (I)I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=2
         0: bipush        10
         2: istore_2
         3: aload_0
         4: dup
         5: getfield      #7                  // Field num:I
         8: iload_1
         9: iadd
        10: putfield      #7                  // Field num:I
        13: aload_0
        14: getfield      #7                  // Field num:I
        17: ireturn
      LineNumberTable:
        line 8: 0
        line 9: 3
        line 10: 13
}
SourceFile: "Main.java"

访问标志 access_flags

紧接在常量池之后的是 访问标志区, 它占用两个字节, 它表示的是,该class文件是类还是接口,是不是public,是不是abstract,是不是final等。

完整的访问标志如下图所示:

2023-09-05-17-19-33-image.png

上面 javap解析的结果中,存在 ACC_PUBLIC ,说明这个类是一个public的。

类索引,父类索引 接口索引计数器

访问标志之后,2个字节为类索引,然后是 2个字节的父类索引,再往后是接口索引计数器

2023-09-05-17-30-35-image.png

对照上文javap的结果日志来看:

类索引为08,父类索引为02,

说明 当前类 为 Main,它的父类是 Object,

接口索引计数器为02 ,则说明这个Main类实现了2个接口。

从这3个部分可以得出,当前类,当前类的父类,当前类实现的接口数量信息。

字段表

紧跟在 接口索引计数器之后的是字段表,它的作用是,描述类和接口中所声明的变量。不包含方法内部的局部变量。

由于字段数量不可确定,所以字段表的开头仍然是一个 2个字节的 字段计数器部分。

2023-09-05-17-46-26-image.png

00 0D 为字段计数器,后面跟的就是 00 0F 字段计数器00 01 变量索引名,和 00 02 变量类型索引

每一个字段表结构如下:

CONSTANT_Fieldref_info{
    u2 access_flag  字段访问标志
    u2 name_index   字段名称索引
    u2 descriptor_index 字段描述索引
    u2 attributes_count 属性计数器
    attribute_info 
}

字段表的开头的2个字节为 字段访问标志

字段访问标志

2023-09-05-19-05-45-image.png

  • 字段表集合中不会出现来自父类或者接口中的字段
  • 内部类为了保持对外部类的访问,会自动添加外部类实例字段

方法表

方法表的开头也是一个 计数器,因为一个类中的方法数量也是不固定的,数量之后就是每一个具体的方法表。此案例中,仅存在一个add方法,但是 实际分析 class文件时,发现了2个方法,因为默认的构造方法也被算进去了。

方法表访问标志

2023-09-05-19-16-07-image.png

Code属性表

方法表之后就是属性表,属性表的开头是 也是属性计数器,接着是 属性表类型索引,这个索引指向的是常量池中名为 Code的属性,这其中主要是保存了一系列字节码。

通过javap命令得出的产物中最后一部分,也就是Code属性表中的内容。比如如下add方法:

public int add(int);
    descriptor: (I)I
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=2
         0: bipush        10
         2: istore_2
         3: aload_0
         4: dup
         5: getfield      #7                  // Field num:I
         8: iload_1
         9: iadd
        10: putfield      #7                  // Field num:I
        13: aload_0
        14: getfield      #7                  // Field num:I
        17: ireturn
      LineNumberTable:
        line 8: 0
        line 9: 3
        line 10: 13

这里包含了很多字节码指令,比如 bipush,istore,ireturn等,这些都是JVM直接执行的字节码指令动作。

总结

本文描述了Class文件结构,并通过简单案例模拟了JVM解析class文件的过程。

Class文件结构中 最重要的是 常量池。它相当于 class文件的资源仓库,其他的结构,多多少少都会引用到这个资源仓库。

平时我们很少会用16进制编辑器(比如windows下的winhex)来打开一个class文件去读那些数字。通常更好的方法是通过javap命令来看class文件结构。

class文件结构,主要包括

  • 标记此文件为class文件格式的 魔数

  • 标记 字节码版本号的 minor version和major version

  • 保存其他数据结构所需的资源的 常量池

    • 常量池以 常量计数器开头,这个数字表示 常量池中 表 的数量,由于是由 2个字节的16进制数表示,所以常量的最大数量为 FFFF, 也就是 2^16。
    • 同样如果在java类中定义了字符串常量,指定了字符串的内容,那么这个字符串被存放在 utf8_info表中,这个表中有一个长度字段,也是 2个字节的16进制数(u2),所以在这种情况下,直接指定字符串常量时 字符串的最大长度是 2^16这个数量级。
  • 描述类和接口中的声明的变量 的

    • 也就是 类的成员变量以及 接口中声明的形参
  • 描述 这个类中所有方法的 方法表

    • 注意:仅仅会包含本类中的方法,继承自父类或者接口的方法不会包含进去
  • 保存字节码指令的 Code

    • 这部分包含了本类中所有的方法转化成的字节码指令,JVM将会严格按照指令顺序去执行每一个方法
  • 字节码中有一种通用设计,那就是 计数器+对象集合,先用计数器字段描述有多少个,然后后面再按照 固定的结构来组装对象。