目录

Jvm 第5篇:类文件结构笔记

开篇说明:本文的重点就是类文件结构,只需要清楚Class文件格式中的各名称的实际意义就行,不用对具体名称下的细节进行深究,否则需要花大量的时间【—-面试导向】。

5.1 无关性的基石

1、统一的程序存储格式:不同平台的虚拟机在所有的平台都统一使用程序存储格式—-字节码(ByteCode);

2、Java编译器可以把Java程序代码编译成虚拟机所需要的Class文件;

3、Java虚拟机不关心Class文件的来源,而只和“Class文件“这种二进制文件格式关联,也就是说Java虚拟机只认识”Class“文件。因此只要能够编译成规范的Class文件的语言都可以在运用Java虚拟机。


5.2 Class文件的结构

下面放一张网上找到的class文件内容,先直观感受下Class文件:【可以使用Sublime Text编辑器打开Class文件】

./1.png

字节码Class类文件是由一些列字节码命令组成,用于表示程序中各种常量、变量、关键字和运算符号的语义等等。

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

Java虚拟机规定,Class文件格式采用一种类似于C语言结构体系的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数

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

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

Class文件结构如下标所示:

类型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count - 1
u2 access_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods methods_count
u2 attributes_count 1
attribute_info attributes attributes_count

Class文件没有任何分隔符,严格按照上面结构表中的顺序排列。无论是顺序还是数量,甚至于数据存储的字节序这样的细节,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。下面将对上表中的各个数据项的实际含义进行讲解。

1、魔数:magic

每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件,即判断这个文件是否符合Class文件规范。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动,而魔数值可以唯一地确定文件的类型。

2、文件的版本:minor_version和major_version

紧接着魔数的4个字节存储的是Class文件的版本号,第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)

Class文件的主、次版本号是由JDK的版本决定的,JDK1.0 ~ JDK1.1使用了45.0 ~ 45.3的版本号(45是主版本号,”.“之和的3是次版本号),从JDK1.1开始,每个大版本的JDK主版本号加1。

高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。

3、常量池:constant_pool_count和constant_pool

紧接着主次版本号之后的是常量次入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是Class文件中第一个出现的表类型数据项目。

constant_pool_count代表Class文件常量池容量计数值。由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据代表常量池容量计数值。

常量池的计数从1开始,因此常量池的容量是constant_pool_count - 1。第0项常量空出来是有特殊考虑的,这样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达”不引用任何一个常量池项目“的含义,这种情况就可以把索引值置为0来表示。

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

1、字面量:比较接近Java语言中的常量的概念,如:文本字符串、声明为final的常量值等;

2、符号引用:属于编译原理方面的概念,包括三类常量:类和接口的全限定名、字段的名称和描述符、方法的名和描述符。

常量池中每一项常量都是一个表,JDK1.7中有14种结构各不相同的表结构数据。这14种表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位(tag),代表这个常量属于哪种常量类型。

./2.png

需要注意一点:

  • length:

1、定义:UTF-8编码的字符串长度是多少;

2、65535限制:Class文件中方法、字段等都需要引用CONSTANT_Utf-8_info型常量来描述名称。所以CONSTANT_Utf-8_info型常量的最大长度也就是Java中方法、字段名的最大长度。而这里的最大长度就是length的最大值,即u2类型能表达的最大值65535(2^32)。所以Java程序中如果定义了超过64KB英文字符的变量或方法名,将会无法编译。

使用:javap - verbose Class文件名,输出Class文件的字节码内容。如下所示:

源文件:TestClass.java

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

输出的字节码文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
C:\Users\pc941\Desktop>javap -verbose TestClass
Classfile /C:/Users/pc941/Desktop/TestClass.class
  Last modified 2018-11-18; size 275 bytes
  MD5 checksum f0aa61ed3167d2e74002b04941d5d7e2
  Compiled from "TestClass.java"
public class TestClass
  SourceFile: "TestClass.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#15         //  java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#16         //  TestClass.m:I
   #3 = Class              #17            //  TestClass
   #4 = Class              #18            //  java/lang/Object
   #5 = Utf8               m
   #6 = Utf8               I
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               inc
  #12 = Utf8               ()I
  #13 = Utf8               SourceFile
  #14 = Utf8               TestClass.java
  #15 = NameAndType        #7:#8          //  "<init>":()V
  #16 = NameAndType        #5:#6          //  m:I
  #17 = Utf8               TestClass
  #18 = Utf8               java/lang/Object
{
  public TestClass();
    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 int inc();
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field m:I
         4: iconst_1
         5: iadd
         6: ireturn
      LineNumberTable:
        line 5: 0
}

4、访问标志:access_flags

在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息。包括:这个Class是类还是接口、是否定义了public类型、是否定义为abstract类型、如果是类,是否被声明为了final等等。

./3.png

5、类索引、父类索引与接口索引集合:this_class、super_class和interfaces

  • 类索引this_class:u2数据类型,用于确定类的全限定类名;
  • 父类索引super_class:u2数据类型,用于确定父类的全限定类名;
  • 接口索引集合interfaces:用于确定接口的全限定名,由于java中可以实现多个接口,因此使用interface_count来存储接口数量。

java.lang.Object类的索引为0(没有父类),其他所有的Java类的父索引都不为0,因为所有的Java类都继承了Object类。实际上类的索引就是描述了Class的extends和implements的关系。

6、字段表集合:field_info、fields_count

**字段表(field_info)**用于描述接口或者类中声明的变量。字段(field)包括类级变量和实例级变量,但是不包括在方法内部声明的局部变量。

  • 描述的信息包括:
  1. 作用域:public、private、protected
  2. 实例还是类变量:static
  3. 可变性:final
  4. 并发可见性:volatile
  5. 是否可序列化:transient
  6. 字段数据类型:基本类型、对象、数组等
  7. 字段名称

fields_count字段数目:表示Class文件的类和实例变量总数。

  • 字段表集合原则
  1. 不会列出超类、父类或父类接口继承而来的字段;
  2. 有可能列出原本Java代码中不存在的字段(内部类会自动添加外部类实例的字段,才能引用到外部类);
  3. Java语言中的字段是无法重载的。

7、方法表集合:methods、methods_count

和字段表集合差不多,方法表集合用来描述Class文件中的方法,但是访问标志和属性表集合和字段表集合有所区别。

  • 访问标志
  1. volatile、transient关键字不可以修饰方法,方法表中少了这两种标志;
  2. synchronized、native、strictfp和abstract可以修饰方法,故方法表增加了这些对应的标志。
  • Code属性
  1. 方法体中的代码都放在了”Code“属性里了。
  • 方法集合原则
  1. 如果父类方法没有在子类中重写(Override),则方法表集合中就不会出现来自父类的方法信息;
  2. 编译器有可能自动添加一些方法,如类构造器方法和实例构造器
  3. 重载(Override)一个方法,需要与原方法具有相同的方法名称但是不同的特征签名,特征签名就是一个方法中各参数在常量池中的字段符号引用集合。

8、属性表集合:attributes、attributes_count

attribute_info属性表是Class文件格式中最具有扩展性的一种数据项目,用于存放field_info字段表、method_info方法表以及Class文件的专有信息,属性表不要求各个属性有严格的顺序,只要求不与已有的属性名字重复即可。属性表中存放的常用信息如下:

属性名称 使用位置 含义
Code 方法表 Java代码编译后的字节码指令存储在Code属性里
Exception 方法表 方法描述时在throws后抛出的异常
LineNumberTable Code属性 Java源码行号和字节码指令行号之间的对应关系
LocalVariableTable Code属性 用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系
SourceFile 类文件 记录Class文件的源码文件名称
ConstantValue 字段表 通知虚拟机自动为静态变量赋值,被static关键字修饰的类变量才可以使用
InnerClasses 类文件 记录内部类与宿主类之间的关系
Deprecated 类、方法、字段表 被声明为Deprecated的类、方法或者字段表示被程序作者定为不再推荐使用
Synthetic 字段、方法 代表此字段或者方法不是由Java源码直接产生的,而是由编译器自行添加的
StackMapTable Code属性 在虚拟机类加载字节码验证被新类型检查验证器使用
Signature 类、接口、初始化方法或者成员的泛型签名 Signature会为它们记录泛型签名信息
BootstrapMethods 类文件 用于保存invokedynamic指令引用的引导方法限定符

下面对上表中的Code属性进行进一步讲解:

Code(存储Java编译后的字节码文件)

  • max_stack:操作数栈深度的最大值,JVM运行时根据这个值来分配栈帧中的操作栈深度;
  • max_locals:代表了局部变量表所需要的内存空间。

(1)Slot:虚拟机为局部变量分配内存的最小单位;

byte、char、float、int、short、boolean、returnAddress:长度少于32位,占1个slot;

double、long:64位。占2个slot。

(2)当代码超出一个局部变量的作用域时,这个局部变量所占用的slot可以被其他的局部变量所使用。

  • code_length:字节码长度
  • code:存储字节码指令
  • 65535限制:虚拟机规定了一个方法不允许超过65535条字节码,否则编译不通过
  • 执行:执行过程中的数据交换、方法调用等操作都基于栈的
  • this关键字:在实例方法中通常可以有this关键字来引用当前对象的变量,这是因为Java编译时在局部变量表中自动增加了这个(this)局部变量。

5.3 字节码指令简介

1、字节码的组成

  • 操作码(Opcode):i (助记符),代表Int类型数据操作…….等等;
  • 操作数(Operands):永远都是一个数组类型的对象。

Java虚拟机采用面向操作数栈而不是寄存器的架构,字节码指令集是一种指令集架构。放弃了操作数对齐,省略了填充的符号和间隔。

2、加载和存储指令

将数据在栈帧中的局部变量和操作数栈之间来回传输。

  • 将一个局部变量加载到操作栈;
  • 将一个数值从操作数栈存储到局部变量表;
  • 将一个常量加载到操作数栈;
  • 扩充局部变量表的访问索引的指令;

3、运算指令

  • 将两个操作数栈上的值进行某种特定运算,并把结果重新存入操作数栈;
  • Java虚拟机没有直接支持byte、short、char、boolean类型,都转为int类型进行运算,使用int的指令代替。

4、类型转换指令

  • 宽化转换 小范围 –> 大范围
  • int到long、float、double
  • long到float、double
  • float到double
  • 窄化转换 大范围 –> 小范围
  • 必须显示的声明转换;
  • 有益处或者精度丢失的情况,但是不会抛出异常。

5、同步指令

  • Java虚拟机支持方法级同步和方法内部一段指令序列的同步,这两种同步结构都使用**管程(Monitor)**来支持;
  • 如果设置了synchronized同步方法,那么执行线程就要求先成功持有“管程”,然后才能执行方法,最后方法执行完成后,再释放“管程”;
  • Java虚拟机通过monitorenter和monitorexit两个指令配对使用,另外编译器会自动增加一个异常处理器。当出现异常时,这个异常处理器能够捕获到所有的异常,并且释放“管程”,mointorexit指令响应。因此,monitorenter和monitorexit这两个指令总是成对出现。

5.4 总结

之所以说一些“非Java”语言也是可以在JVM上运行,这是因为JVM只认识Class文件,所以如果某种语言最终编译出来的文件是Class文件,那么对于JVM来说就是没有区别的,但前提是按照Class文件的结构来,不然也无法正常执行。Class定义了许多特定的基本数据类型和表结构,通过魔数让JVM认识该文件,版本号保证可以在要求的JDK版本上运行,在常量池中定义好常量,访问标志位确定访问权限,索引集合方便与外界Class保持联系,字段表保存我们定义好的变量,方法表存储方法的信息,属性表存储了上诉各种表中的属性。其中记住Slot是局部变量分配内存的最小单位,当程序超过作用域的时候,Slot可以被其他替换使用。

到这里,代码还是静态的存储格式,程序要运行起来,还需要操作指令,也是右字节码存储,包括操作码和操作数。操作指令有加载和存储指令、运算指令、类型转换指令已经同步指令等等。