虚拟机类加载机制(四)--- Class文件结构实例详解

Java的技术体系包括

  • 支持Java程序运行的虚拟机(JVM)
  • 提供接口支持的Java API
  • Java 编程语言
  • 第三方Java框架(如Spring等)

代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,确实编程语言的一大步。


前面几篇文章我们详细介绍了java类编译后的Class文件的文件结构,这篇文章我们通过一个最简单的实例来分析一下Class文件的的结构,帮助我们更好的熟悉。

我们先写一个简单的Java类,再通过javac命令将文件编译成class文件

package org.fenixsoft.clazz;

public class TestClass{

    private int m;

    public int inc(){
        
        return m + 1;

    }
}

编译之后的class文件如下图所示


class文件结构翻译

接下来我们来详细分析一下这个Class文件

魔数

由上图可知,首先我们看到前四个字节“cafe babe”这就是我们所说的魔数,代表这个16进制的文件是一个class文件。

版本号

紧接着的四位是“0000 0034”是该class文件的版本号,换算成十进制就是52,代表该class文件是用jdk1.8编译而成的。

常量池

紧接着后面便是常量池部分,常量池最开始的两位“0013”即表示常量池的数量,换算成十进制为19,由于常量池中的常量是从1开始计数的,0号常量为系统保留,所以后面紧跟着19-1=18个常量,常量池内常量的总数是18个。下面我们逐个分析一下这18个常量

  1. 我们看第一个常量的第一个字节,也就是我们前面提到的,每个常量最开头都有一个u1类型的tag表示该常量是一个什么常量,此处为“0a"也就是第10个类型,查上表我们知道10代表的常量类型是CONSTANT_Methodref_info,表示类中方法的符号引用。该常量占用5个字节,它的结构如下
类型 名称 数量 描述
u1 tag 1 属于什么常量类性
u2 index 1 指向声明方法的类描述符CONSTANT_Class_info的索引项
u2 index 1 指向名称及类型描述符CONSTANT_NameAndType_info的索引项
  1. 第二个常量的tag是“09”,查表可知表示的是一个 CONSTANT_Fieldref_info 类型,意思为字段的符号引用。该常量也是5个字节,它的结构如下
类型 名称 数量 描述
u1 tag 1 属于什么常量类性
u2 index 1 指向声明字段的类描述符CONSTANT_Class_info的索引项
u2 index 1 指向字段的描述符CONSTANT_NameAndType_info的索引项
  1. 紧接着下一个常量的tag是“07”,查表可知表示的是一个 CONSTANT_Class_info 类型,意为类或者接口的符号引用。该常量是3个字节,它的结构如下
类型 名称 数量 描述
u1 tag 1 属于什么常量类性
u2 name_index 1 指向类的全限定名的索引

此处的值为"00 11",意为指向第17个常量。

  1. 再往下还是一个 07 类型的常量,依然是3个字节,指向常量池中索引为 0x0012(十进制为18) 的位置。
  2. 常量 tag 为01,查表可知是一个 CONSTANT_Utf8_info 常量,即UTF8编码的字符串。该常量长度不定,结构如下
类型 名称 数量 描述
u1 tag 1 属于什么常量类性
u2 length 1 字符串占用的字节总数
u1 bytes length 字符串

从图可知该常量类型为4个字节,表示的字符串长度为1,utf编码表为0x6d,大家可以去查一下表,可知该字符为“m",也就是我们定义的变量m。

  1. 依然是一个长度为4字节的 CONSTANT_Utf8_info 常量,表示的字符串长度为1,utf8编码为 0x49。该字符为“I"。
  2. CONSTANT_Utf8_info 类型常量,字符串长度为6字节,总长度为9字节,为 "3c 69 6e 69 74 3e",表示的意思为 ""。
  3. CONSTANT_Utf8_info 类型常量,字符串长度为3字节,总长度为6字节,为 "28 29 56",表示的意思为 "()V"。
  4. CONSTANT_Utf8_info 类型常量,字符串长度为4字节,总长度为7字节,为 "43 6f 64 65",表示的意思为 "Code"。
  5. CONSTANT_Utf8_info 类型常量,字符串长度为15字节,总长度为18字节,为 "4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65",表示的意思为 "LineNumberTable"。
  6. CONSTANT_Utf8_info 类型常量,字符串长度为3字节,总长度为6字节,为 "69 6e 63",表示的意思为 "inc"。
  7. CONSTANT_Utf8_info 类型常量,字符串长度为3字节,总长度为6字节,为 "28 29 49",表示的意思为 "()I"。
  8. CONSTANT_Utf8_info 类型常量,字符串长度为10字节,总长度为13字节,为 "53 6f 75 72 63 65 46 69 6c 65",表示的意思为 "SourceFile"。
  9. CONSTANT_Utf8_info 类型常量,字符串长度为14字节,总长度为17字节,为 "54 65 73 74 43 6c 61 73 73 2e 6a 61 76 61",表示的意思为 "TestClass.java"。
  10. CONSTANT_NameAndType_info 类型常量,占用5个字节的长度,表示的含义为“字段或者方法的符号引用”,它的结构如下
类型 名称 数量 描述
u1 tag 1 属于什么常量类性
u2 index 1 常量索引,指向字段或者方法的名称
u2 index 1 常量索引,指向字段或者方法的描述符

此处的值为"0c 0007 0008",表示名称为指向第7个常量的内容,描述符为指向第8个常量的内容。查上文可知,为 " ()V",表示类的实例构造器同时返回值为void

  1. CONSTANT_NameAndType_info 类型常量,与上一个常量类型相同,占用5个字节的长度,此处的值为 "0c 0005 0006",查上文可知表示的含义为 "m I",即我们定义的 int 型变量 m
  2. CONSTANT_Utf8_info 类型常量,字符串长度为29字节,总长度为32字节,字符长内容为 "6f 7267 2f66 656e 6978 736f 6674 2f63 6c61 7a7a 2f54 6573 7443 6c61 7373",表示的意思为 "org/fenixsoft/clazz/TestClass"。
  3. CONSTANT_Utf8_info 类型常量,字符串长度为16字节,总长度为19字节,字符长内容为 "6a 6176 612f 6c61 6e67 2f4f 626a 6563 74",表示的意思为 "java/lang/Object"。

以上便是18个常量的具体内容。常量池的内容到此结束。

访问标志

紧接着常量池的是一个u2类型的访问标志access_flags,此处为"00 21",可知表示的含义是,这个类是一个 public 类型

类索引

紧接着的是一个u2类型的类索引,此处值为"00 03",意为指向常量池的第三个常量的内容,即为该类的名称(全限定名),查上文可知类名为 "org/fenixsoft/clazz/TestClass"。

父类索引

紧接着的是一个u2类型父类索引,此处值为 "00 04",意为指向常量池的第四个常量,即为父类的名称,查上文可知父类名为 "java/lang/Object"。

接口索引集合

紧接着的是接口索引集合,前两个字节表示接下来接口的数量,此处为 "0000",表示没有接口。

字段表集合

紧接着的是字段表集合,前两个字节表示字段的数量,此处为 "0001",表示有一个字段(变量)。接着表示的是字段,一个字段的结构前文已经讲过,为方便查看,结构如下

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attributes_info attributes attribute_count

此处的值为 "0002 0005 0006 0000",逐字翻译为,该变量的访问标志为 private,名称为指向常量池第5个常量为 m,变量的描述符为指向常量池的第6个常量 I,即为int型,属性表为空。翻译过来就是 "private int m;"。与我们源码中定义的相同。

方法表集合

紧接着的是方法表集合,前两个自己表示的是方法的数量,此处为"0002"即有两个方法。方法表的结构前文也已经讲过,再次将它的结构放在下面,与字段表的结构一样

类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attributes_info attributes attribute_count

此处第一个方法表的前8个字节,值为 "0001 0007 0008 0001",逐字翻译为,该方法的访问标志为 public,名称为指向常量池第7个常量,查上文可知为 "",描述符为"()V",翻译过来就是 "public void "。可知该类默认的构造器,即编译器自动为该类加上的编译器。

紧接着 "0001" 表示有一个属性,属性表的前两个字节表示属性名称,此处为 "0009",即"Code",表示构造器方法的内部逻辑。紧接着的4个字节表示接下来属性表的长度,此处为 "0000001d",意为29个字节。其后紧跟的29个字节即为 "Code" 属性表的内容。Code属性表的结构,我们前面也讲过,再复制如下,

类型 名称 数量 描述
u2 attribute_name_index 1 属性名称
u4 attribute_length 1 属性总长度
u2 max_stack 1 操作数栈最大深度
u2 max_locals 1 局部变量表所需要的存储空间,编译器会根据局部变量的作用域等计算出该值的大小
u4 code_length 1 字节码长度
u1 code code_length 用来存储字节码指令的一系列字节流
u2 exception_table_length 1 异常表长度
exception_info exception_table exception_table_length 显示异常处理表
u2 attribute_count 1
attribute_info attributes attributes_count

此处值为 "00 0100 0100 0000 052a b700 01b1 0000 0001 000a 0000 0006 0001 0000 0003"。我们逐个翻译一下,
"0001"表示操作栈的最大深度,此处max_stack=1,紧接着"0001"表示局部变量表的最大深度,此处为max_locals=1,紧接着四个字节为"00 0000 05"表示字节码长度为5个字节,紧接着的5个字节为"2a b700 01b1",查字节码指令表翻译可得为

aload_0
invokespecial
return

紧接着的2个字节为异常表的长度,此处为"0000"表示有0个异常表。紧接着"0001"表示有1个属性表,紧接着的两个字节为"000a"表示属性的名字为指向常量池第10个常量,为"LineNumberTable",紧接着的4个字节为"0000 0006"表示属性表的长度为6个字节,接下来的6个字节就是LineNumberTable属性表,它用来描述java代码行号与字节码行号之间的关系,它的结构前文也讲过,如下

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 line_number_table_length 1
line_number_info line_number_table line_number_table_length

此处值为 "0001 0000 0003",意为有1个line_number_info表,表的内容翻译为该方法字节码指令的第0行,对应java代码的第3行。

以上便是这个方法的全部解读。再来看下一个方法

紧接着的8个字节为"0001 000b 000c 0001",表示该方法为public,名称为指向常量池第11个常量为"inc",描述符为指向常量池第12个常量为"()I",表示该方法的返回值为int型,翻译得"public int inc()"。

紧接着 "0001" 表示有一个属性,属性表的前两个字节表示属性名称,此处为 "0009",即"Code",表示构造器方法的内部逻辑。紧接着的4个字节表示接下来属性表的长度,此处为 "0000 001f" 即为31个字节。此处的31个字节为 "0002 0001 0000 0007 2ab4 0002 0460 ac00 0000 0100 0a00 0000 0600 0100 0000 09"。我们逐个翻译一下,
"0002"表示操作栈的最大深度,此处max_stack=2,紧接着"0001"表示局部变量表的最大深度,此处为max_locals=1,紧接着四个字节为"0000 0007"表示字节码长度为7个字节,紧接着的5个字节为"2ab4 0002 0460 ac",查字节码指令表翻译可得为

aload_0
getfield
iconst_1
iadd
ireturn

紧接着的2个字节为异常表的长度,此处为"0000"表示有0个异常表。紧接着"0001"表示有1个属性表,紧接着的两个字节为"000a"表示属性的名字为指向常量池第10个常量,为"LineNumberTable",紧接着的4个字节为"0000 0006"表示属性表的长度为6个字节,接下来的6个字节就是LineNumberTable属性表,它用来描述java代码行号与字节码行号之间的关系,此处值为"00 0100 0000 09",意为有1个line_number_info表,表的内容翻译为该方法字节码指令的第0行,对应java代码的第9行。

至此方法表中的两个方法已经解读完毕。

属性表集合

紧接着往后是类的属性表集合,其后的两个字节为"0001",表示有一个属性表attribute_info,它的名称为"000d"即指向常量池第13个常量为 SourceFile ,属性用于记录生成这个Class文件的源码文件名称,该属性的长度为"0000 0002"个字节,其后的两个字节 "000e" 代表常量池第14个常量,即"TestClass.java",即为源文件的文件名。

总结

以上便是我们根据一个最简单的Class文件的16进制文本内容,逐一翻译而来的该类的含义。当然JDK为我们提供了分析Class文件的命令工具,命令如下

javap -v TestClass.class

在此例中,通过上述命令,可以得到如下的解析结果

javap解析结果

不积跬步无以至千里,再复杂的类也是由上面的规则演化而来,也可以用同样的方式解读出来。

你可能感兴趣的:(虚拟机类加载机制(四)--- Class文件结构实例详解)