《深入理解Java虚拟机》——类文件结构之魔数常量池

时间:2022-12-27 21:41:49

相对于Java虚拟机的其他部分,这部分的内容我们只需要搞清楚下面两个方面的内容:

1.无关性

2.Class文件的结构与组成

我们都知道Java有个特性是:一次编写,到处运行。这里体现的是平台无关性,但是对于Java虚拟机来说,不仅仅是具有平台无关性的特点,还具有语言无关性的特性。

各种不同平台的虚拟机与所有平台都统一使用的程序存储格式——字节码(ByteCode)是构成平台无关性的基石。实现语言无关性的基础仍然是虚拟机和字节码存储格式。Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。基于安全方面的考虑,Java虚拟机规范要求在Class文件中使用许多强制性的语法和结构化约束,但任一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效的Class文件。作为一个通用的、机器无关的执行平台,任何其他语言的实现者都可以将Java虚拟机作为语言的产品交付媒介。如图所示:

《深入理解Java虚拟机》——类文件结构之魔数常量池《深入理解Java虚拟机》——类文件结构之魔数常量池


另外需要注意的是Java语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成的,因此字节码命令所能提供的语义描述能力肯定会比Java语言本身更加强大。因此,有一些Java语言本身无法有效支持的语言特性不代表字节码本身无法有效支持,这也为其他语言实现一些有别于Java的语言特性提供了基础。


Class类文件的结构

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。

根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表,后面的解析都要以这两种数据类型为基础。

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

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

《深入理解Java虚拟机》——类文件结构之魔数常量池

《深入理解Java虚拟机》——类文件结构之魔数常量池

无论是符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计算器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。这里需要注意的是:Class的结构不像XML等描述语言,由于它没有任何分隔符号,所以在上表中,无论是顺序还是数量,甚至于数据存储的字节序(Byte Ordering,Class文件中字节序为Big-Endian)这样的细节,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。

为了理解清楚Class文件结构,这里我跟随书的例子来阐述(该Demo使用jdk1.7编译输出):

 
 
 
  1. package com.general.class_structure;
  2. public class TestClass {
  3. private int m;
  4. public int inc(){
  5. return m+1;
  6. }
  7. }

接着给出Class文件以及十六进制的文件:

《深入理解Java虚拟机》——类文件结构之魔数常量池


《深入理解Java虚拟机》——类文件结构之魔数常量池

《深入理解Java虚拟机》——类文件结构之魔数常量池

《深入理解Java虚拟机》——类文件结构之魔数常量池

1.魔数

每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准中都使用魔数来进行身份识别,使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。文件格式的制定者可以*地选择魔数值,只要这个魔数值还没有被广泛采用过同时又不会引起混淆即可。Class文件的魔数值为0xCAFEBABE。紧接着魔数的4个字节存储的是Class文件的版本号:第5个和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。Java的版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1(JDK1.0~1.1使用了45.0~45.3的版本号),高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。如上图中前8个字节:CA FE BA BE 00 00 00 33


2.常量池

紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的,在Class文件格式规范制定之时,设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以把索引值置为0来表示。Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始的。


常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。


字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,主要包括了下面三类常量:

类和接口的全限定名(Fully Qualified Name)

字段的名称和描述符(Descriptor)

方法的名称和描述符


Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。


常量池中每一项常量都是一个表,在JDK1.7之前共有11种结构各不相同的表结构数据,在jdk1.7中为了更好地支持动态语言调用,又额外增加了3种(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info)。这14种表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位,代表当前这个常量属于哪种常量类型。这14种常量类型所代表的具体含义见下图:

《深入理解Java虚拟机》——类文件结构之魔数常量池

《深入理解Java虚拟机》——类文件结构之魔数常量池

《深入理解Java虚拟机》——类文件结构之魔数常量池

之所以说常量池是最烦琐的数据,是因为这14种常量类型各自均有自己的结构。

《深入理解Java虚拟机》——类文件结构之魔数常量池

《深入理解Java虚拟机》——类文件结构之魔数常量池

对于常量池的内容大概就这么多,下面我们结合之前给出的demo来解析一下上文中的16进制字节码,以便更好的理解类文件中的常量池以及类文件结构,另外需要说明一点的是这里所说的类均指xxx.class文件而不是xxx.java文件。


0700 0201 0025 636f 6d2f 6765 6e65 7261 6c2f 636c 6173 735f 7374 7275 6374 7572 652f 5465 7374 436c 6173 73

07与01就是指上面所说的常量类型。

0700 02代表了一个类型为CONSTANT_Class_info的常量,大家可以在上面找出这种常量的结构,其中0002代表指向常量池中的第二个常量,即后面的010025....617373

01 0025 636f 6d2f 6765 6e65 7261 6c2f 636c 6173 735f 7374 7275 6374 7572 652f 5465 7374 436c 6173 73 代表了一个类型为CONSTANT_Utf8_info的常量,01代表常量类型,0025是指字符串长度,636f......617373的值为:com/general/class_structure/TestClass。


自己人工解读16进制的字节码文件总是有点自虐的,还好jdk给我们提供了javap工具来解读类文件,比如我们的例子,在命令行下输入相应命令:

《深入理解Java虚拟机》——类文件结构之魔数常量池

《深入理解Java虚拟机》——类文件结构之魔数常量池

javap -v xxx.class输出的信息如下:

 
 
 
  1. xxxxxxx/com/general/class_structure$ javap -v TestClass.class
  2. Classfile xxxxxx/TestJVMDemo/bin/com/general/class_structure/TestClass.class
  3.  Last modified 2017-10-11; size 409 bytes
  4.  MD5 checksum 4f90a8afb819eac2dcef42a2b02ae603
  5.  Compiled from "TestClass.java"
  6. public class com.general.class_structure.TestClass
  7.  SourceFile: "TestClass.java"
  8.  minor version: 0
  9.  major version: 51
  10.  flags: ACC_PUBLIC, ACC_SUPER
  11. Constant pool:
  12.   #1 = Class              #2             //  com/general/class_structure/TestClass
  13.   #2 = Utf8               com/general/class_structure/TestClass
  14.   #3 = Class              #4             //  java/lang/Object
  15.   #4 = Utf8               java/lang/Object
  16.   #5 = Utf8               m
  17.   #6 = Utf8               I
  18.   #7 = Utf8               <init>
  19.   #8 = Utf8               ()V
  20.   #9 = Utf8               Code
  21.  #10 = Methodref          #3.#11         //  java/lang/Object."<init>":()V
  22.  #11 = NameAndType        #7:#8          //  "<init>":()V
  23.  #12 = Utf8               LineNumberTable
  24.  #13 = Utf8               LocalVariableTable
  25.  #14 = Utf8               this
  26.  #15 = Utf8               Lcom/general/class_structure/TestClass;
  27.  #16 = Utf8               inc
  28.  #17 = Utf8               ()I
  29.  #18 = Fieldref           #1.#19         //  com/general/class_structure/TestClass.m:I
  30.  #19 = NameAndType        #5:#6          //  m:I
  31.  #20 = Utf8               SourceFile
  32.  #21 = Utf8               TestClass.java
  33. {
  34.  public com.general.class_structure.TestClass();
  35.    flags: ACC_PUBLIC
  36.    Code:
  37.      stack=1, locals=1, args_size=1
  38.         0: aload_0      
  39.         1: invokespecial #10                 // Method java/lang/Object."<init>":()V
  40.         4: return        
  41.      LineNumberTable:
  42.        line 3: 0
  43.      LocalVariableTable:
  44.        Start  Length  Slot  Name   Signature
  45.               0       5     0  this   Lcom/general/class_structure/TestClass;
  46.  public int inc();
  47.    flags: ACC_PUBLIC
  48.    Code:
  49.      stack=2, locals=1, args_size=1
  50.         0: aload_0      
  51.         1: getfield      #18                 // Field m:I
  52.         4: iconst_1      
  53.         5: iadd          
  54.         6: ireturn      
  55.      LineNumberTable:
  56.        line 6: 0
  57.      LocalVariableTable:
  58.        Start  Length  Slot  Name   Signature
  59.               0       7     0  this   Lcom/general/class_structure/TestClass;
  60. }

大家可以在上面的输出信息中清楚地看到,常量池的个数、类型,值。大家看到上面的常量池也许会懵逼,我们前面讲了常量池包含字面量和符号引用,字面量可以理解为Java语言层面的常量,而符号引用包含三类常量:(1)类和接口的全限定名(2)字段的名称和描述符(3)方法的名称和描述符。常量池中的<init>、LineNumberTable、I等这些是什么鬼,代码里我们没有写啊,其实这部分常量是自动生成的,会在后面的字段表、方法表、属性表引用到。


最后说一下javap,javap是java class文件分解器,可以反编译(即对javac编译的文件进行反编译),也可以查看java编译器生成的字节码。用于分解class文件。

为了避免篇幅过长,关于类文件结构的相关总结不打算写在一篇文章里了,这篇文章主要说明了无关性、魔数、常量池。接下来的文章会继续介绍Class文件结构中的其他内容。


转载请注明出处:http://blog.csdn.net/android_jiangjun/article/details/78204427