Java的class字节码结构

情景

问:Java中的String的字符串长度有限吗?

答:我知道茴的四种写法,你看啊。。。。

问:。。。。

探索

为了了解这问题,我们需要区探究一下class文件

class文件又叫字节码文件,是它为java实现了跨平台运行的能力。

字节码也解除了虚拟机和java之间的耦合,因为java虚拟机可以支持其他语言上生成的字节码,例如JRuby,Groovy。

从纵观角度来看,class文件只有两种数据结构:无符号数

  • 无符号数:属于基本的数据类型,以u1,u2,u4,u8来分别代表1字节2字节4字节8字节的无符号数,他可以用来描述数字、索引引用、数值或者UTF-8字符串编码。
  • :表是由多个无符号数或者其他表结构组合的复合数据类型,class文件中的所有的表结构都以_info结尾。其实class文件就是一张表。

这些结构按照预先规定好的顺序紧密的相连,结构顺序如下所示:

当jvm加载某个class文件时,jvm就是根据上图的结构区解析class文件,并加载到内存中,并根据下图的情况分配内存空间。

用实例分析

首先编写一段简单的java代码

import java.io.Serializable;
public class Test implements Serializable, Cloneable{
      private int num = 1;
      public int add(int i) {
          int j = 10;
          num = num + i;
          return num;
     }
}

然后通过javac生成Test.class,然后用16进制编译器打开:

  • 魔数:开头的四位 ca fe ba be,他是固定值,用来判断是否标准class文件
  • 版本号00 00 00 34,前两位代表次版本号(minor_version),后两位代表主版本号(major_version)。说明当前的版本号是52.0对应jdk1.8.0
  • 常量池:它是一个叫做常量池的表(cp_info),在常量池中保存了各种类的信息,比如类的名称、父类、方法、参数类型等。

常量池中的每一个类项都是一个表,共有14种类型,如下:

常量池中的每一项都会有一个u1大小的tag值,用于标记当前数据结构属于哪一种表。

我们以CONSTANT_Class_infoCONSTANT_Utf8_info两张表举例说明:

table CONSTANT_Class_info {
    u1  tag = 7;
    u2  name_index;
}
  • tag:占用一个字节大小。比如值为 7,说明是 CONSTANT_Class_info 类型表。
  • name_index:是一个索引值,可以将它理解为一个指针,指向常量池中索引为 name_index 的常量表。比如 name_index = 2,则它指向常量池中第 2 个常量。

接下来我们来看看CONSTANT_Utf8_info的表结构:

table CONSTANT_utf8_info {
    u1  tag;
    u2  length;
    u1[] bytes;
}
  • tag:值为1 ,表示是CONSTANT_Utf8_info
  • length:表示u1[]数组的长度,例如length=5,则表示接下来5个连续的u1类型数据
  • bytes:u1 类型数组,长度为上面第 2 个参数 length 的值。

我们java代码声明的String字符串最终的存储格式就是CONSTANT_Utf8_info,因此length最大能表示的长度就是u2能代表的最大值65536个,但是需要额外的两个字节来保存null值,因此String所能表示的最大长度是65536-2=65534

不难看出常量池内部的表中也有相互之间的引用,用一张图来表示CONSTANT_Class_infoCONSTANT_Utf8_info表格间的关系

理解了常量池内部的数据结构之后,我们看一下实例代码解析过程。从版本号之后开始解析:

  • 00 1d:说明常量计数器的值为29,由于下标为0的常量被JVM使用,我们实际上常量池的大小为28.

  • ** 0a**:可以查到表类型为CONSTANT_Methodref_info,因此常量池中的第一个常量类型为方法引用表。其结构如下:

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

}
  • 00 06:指向常量池中的第6个常量
  • 00 15:指向常量池中的第21个常量

第一个表已经解读完了,接下来时第二个表

  • 09:当前时字段引用表,结构如下
CONSTANT_Fieldref_info{
    u1 tag;
    u2 class_index;        指向此字段的所属类
    u2 name_type_index;    指向此字段的名称和类型
}
  • 00 05 :指向常量池中第 5 个常量。
  • 00 16:指向常量池中第 22 个常量。

我们已经解析了常量池中的两个常量, 后面的常量解析方法如出一辙,实际上我们可以借助javap命令来查看常量池中的内容

javap -v Test.class

其结果正如我们前面解析的一样,其中下标为21的常量类型为NameAndType,它的数据结构是

CONSTANT_NameAndType_info{
    u1 tag;
    u2 name_index;    指向某字段或方法的名称字符串
    u2 type_index;    指向某字段或方法的类型字符串

}
  • 而下标在 21 的 NameAndTypename_indextype_index 分别指向了 13 和 14,也就是“()V”。因此最终解析下来常量池中第 1 个常量的解析过程以及最终值如下图所示:

经过仔细分析,我们可以知道常量池中第一个常量保存的是Object中的默认构造方法。

  • 常量池之后,是访问标志00 21,它占两个字节,前面的表中有白框标识:它代表了类或者方法的修辞方式,含义如下图所示。
  • 00 05:类索引
  • 00 06: 父类索引
  • 00 02 :接口索引计数器

回顾常量池

类名是Test,父类是Object,我们接着看接口计数器为2 说明下面的4个字节描述了两个接口

  • 00 07:常量值为"Serializable"
  • 00 08 :常量值为"Cloneable"

综上:当前类为 Test 继承自 Object 类,并实现了“Serializable”和“Cloneable”这两个接口。

接下里是字段表

  • 00 02 :字段计数器:表示类中声明类两个字段,接着回出现两个字段表的数据结构,字段表数据结构如下:
CONSTANT_Fieldref_info{
    u2  access_flags    字段的访问标志
    u2  name_index          字段的名称索引(也就是变量名)
    u2  descriptor_index    字段的描述索引(也就是变量的类型)
    u2  attributes_count    属性计数器
    attribute_info
}
  • 00 02:字段访问标志,代表是private类型,解析如下图所示:
  • 00 09:变量名索引,变量名是num
  • 00 0a:变量类型,类型是I,说明是int类型变量

接下来的解析如出一辙,我们说一下注意事项。

  1. 字段表集合中不会列出从父类或者父接口中继承而来的字段。

  2. 内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

方法表

方法表紧随字段表其后,也是从一个计数器开始

方法表的结构如下:

CONSTANT_Methodref_info{
    u2  access_flags;        方法的访问标志
    u2  name_index;          指向方法名的索引
    u2  descriptor_index;    指向方法类型的索引
    u2  attributes_count;    方法属性计数器
    attribute_info attributes;
}

访问标志的值如下:

第一个方法是构造方法,我们主要分析一下add方法:

从图中我们可以看出 add 方法的以下字段的具体值:

  1. access_flags = 00 01 也就是访问权限为 public。
  2. name_index = 00 11 指向常量池中的第 17 个常量,也就是“add”。
  3. type_index = 00 12 指向常量池中的第 18 个常量,也即是 (I)。这个方法接收 int 类型参数,并返回 int 类型参数。

属性表:在之前解析字段和方法的时候,在它们的具体结构中我们都能看到有一个叫作 attributes_info 的表,这就是属性表。

属性表并没有一个固定的结构,各种不同的属性只要满足以下结构即可:

CONSTANT_Attribute_info{
    u2 name_index;
    u2 attribute_length length;
    u1[] info;
}

我们接着往下看:

  • 00 01:属性计数器,说明只有一个属性
  • 00 0f:属性索引,通过查看常量,我们可以看出它是code属性表
code属性表中,最主要的就是一系列的字节码。通过`javap -v Test.class`我们可以查看到方法的字节码

JVM执行add方法时,就是通过这一系列的指令操作来完成的。

解答

String能保存的最大长度需要从两个角度来回答,在编译期还是运行期。

编译期由于是CONSTANT_Utf8_info格式存储的,所以最大长度是65534字节,这里需要注意,英文和数字是占用1字节,而汉字是占用两个字节,所以不一定能存到最大长度的字符。

运行期时字符串的内部是有char数组的value来存储的,数组的长度表示类型是int类型,所以这时候String的最大长度是Integer.MAX_VALUE (2147483647)了,大约运行时需要4BG内存才能达到最大最大字符串长度。

你可能感兴趣的:(Java的class字节码结构)