【深入理解java虚拟机】 - class类文件结构

文章目录

  • 概述
    • 平台无关性
  • Class类文件的结构
    • class文件格式
    • class文件结构
      • 魔数
      • 文件版本号
      • 常量池
      • 访问标识
      • 类索引、父类索引与接口索引集合
      • 字段表集合
      • 方法表集合
      • 属性表集合

概述

“一次编写,到处运行 (Write Once,Run Anywhere)”。

平台无关性

Oracle公司以及其他虚拟机发行商发布过许多可以运行在各 种不同硬件平台和操作系统上的Java虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节 码,从而实现了程序的“一次编写,到处运行”。

实现语言无关性的基础是虚拟机字节码存储格式。Java虚拟机不与包括Java语言在内的任何程序语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机 指令集、符号表以及若干其他辅助信息。

无论使用何种语言进行软件开发,只要能将源文件编译为正确的Class文件,那么这种语言就可以在Java虚拟机上执行。当Java源代码成功编译成字节码后,在不同的平台上面运行,就无须再次编译。

【深入理解java虚拟机】 - class类文件结构_第1张图片

Class类文件的结构

Java虚拟机规范—— class文件格式官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html

任何一个Class文件都对应着唯一的一个类或接口的定义信息,但是反过来说,类或接口并不一定都得定义在文件里(如类或接口也可以动态生成,直接送入类加载器中)。

class文件格式

Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在 件之中,中间没有添加任何分隔符,所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。

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

  • 无符号数:无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串 值。
  • 表:表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名 都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视作是一张表。

无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的 容量计数器加若干个连续的数据项的形式,这时候称这一系列连续的某一类型的数据为某一类型的“集合”。

class文件结构

首先打开一个.class文件看一看:
【深入理解java虚拟机】 - class类文件结构_第2张图片
如何打开class文件不乱码?

当我们直接使用记事本或者notepad等打开一个class文件时会乱码,如:
【深入理解java虚拟机】 - class类文件结构_第3张图片> 我们可以使用以下方式正确打开class文件:

  1. 使用sublime编辑器直接打开
  2. 使用notepad++插件打开:
    【深入理解java虚拟机】 - class类文件结构_第4张图片

Class文件的结构并不是一成不变的,随着Java虚拟机的不断发展,总是不可避免地会对Class文件结构做出一些调整,但是其基本结构和框架是非常稳定的。它总体包括以下几个部分:

  • 魔数(magic)
  • class文件版本
  • 常量池
  • 访问标识
  • 类索引、父类索引、接口索引集合
  • 字段表集合
  • 方法表集合
  • 属性表集合
ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

魔数

每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为 一个能被虚拟机接受的Class文件。
【深入理解java虚拟机】 - class类文件结构_第5张图片

  • 魔数固定为 oxcafebabe,不会改变
  • 如果一个class文件不以oxcafebabe开头,虚拟机在进行文件校验的时候会直接抛出错误:
    Error: A JNI error has occurred,please check your installation and try again
    Exception in thread “main” java.lang.ClassFormatError:Incompatible magic value 1885430635 in classfile ***
  • 使用魔数也不是扩展名来识别字节码文件,是因为扩展名可以随意改动。

文件版本号

紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。
【深入理解java虚拟机】 - class类文件结构_第6张图片代表次版本号的第5个和第6个字节值为0x0000,而主版本号的值 为0x0034,也即是十进制的52。

Java的版本号是从45开始的,JDK 1.1之后 的每个JDK大版本发布主版本号向上加1(JDK 1.0~1.1使用了45.0~45.3的版本号),高版本的JDK能 向下兼容以前版本的Class文件,但不能运行以后版本的Class文件。
【深入理解java虚拟机】 - class类文件结构_第7张图片

常量池

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

这部分包含两块结构:常量池容量计数值常量池表

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

【深入理解java虚拟机】 - class类文件结构_第8张图片
这个容量计数是从1而不是0开始的,如上图的常量池容量为十六进制数0x001b,即十进制的27,这就 代表常量池中有26项常量,索引值范围为1~26。

2.常量池表
常量池中主要存放两大类常量:字面量符号引用

  • 字面量:字面量比 较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等
  • 符号引用:符号引用则属于编译 原理方面的概念,主要包括下面几类常量:
    被模块导出或者开放的包、类和接口的全限定名、字段的名称和描述符、方法的名称和描述符、方法句柄和方法类型、动态调用点和动态常量

常量池中每一项常量都是一个表,最初常量表中共有11种结构各不相同的表结构数据,后来为了 更好地支持动态语言调用,额外增加了4种动态语言相关的常量,为了支持Java模块化系统 ,又加入了CONSTANT_Module_info和CONSTANT_Package_info两个常量,所以截至JDK 13,常量表中分别有17种不同类型的常量。

类型 标志 描述
CONSTANT_Utf8_info 1 UTF-8编码的字符串
CONSTANT_Integer_info 3 整型字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT Long_info 5 长整型字面量
CONSTANT_Double_info 6 双精度浮点型字面量
CONSTANT_Class_info 7 类或接口的符号引用
CONSTANT_String_info 8 字符串类型字面量
CONSTANT_Fieldref_info 9 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_ InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的部分符号引用
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_MethodType_info 16 表示方法类型
CONSTANT_Dynamic_info 17 表示一个动态计算常量
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点
CONSTANT_Module_info 19 表示一个模块
CONSTANT_Package_info 20 表示一个模块中开放或者导出的包

访问标识

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

具体的标志位以及标志的含义如下表所示:

类型 标志 描述
ACC_PUBLIC 0x0001 是否为public类型
ACC_FINAL 0x0010 是否被声明为final,只有类可设置
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语义
ACC_INTERFACE 0x0200 标识这是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其他类型值为假
ACC_SYNTHETIC 0x1000 标识这个类并非由用户代码产生的标识
ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举
ACC_MODULE 0x8000 标识这是一个模块

类索引、父类索引与接口索引集合

类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,Class文件中由这三项数据来确定该类型的继承关系。

  • 类索引用于确定这个类的全限定名;
  • 父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多 重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了 java.lang.Object外,所有Java类的父类索引都不为0。
  • 接口索引集合就用来描述这个类实现了哪些接 口,这些被实现的接口将按implements关键字(如果这个Class文件表示的是一个接口,则应当是 extends关键字)后的接口顺序从左到右排列在接口索引集合中。

类索引和父类索引都是一个u2类型的数据,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过 CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的 全限定名字符串。

而接口索引集合 是一组u2类型的数据的集合,索引集合入口的第一项u2类型的数据为接口计数器,表示索引表 的容量。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。

字段表集合

字段表用于描述接口或者类中声明的变量。

  • 字段(field)包括类级变量以及实例级变量,但是不包括方法内部、代码块内部声明的局部变量。
  • 字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。
  • 它指向常量池索引集合,它描述了每个字段的完整信息。比如字段的标识符、访问修饰符(public、private或protected)、是类变量还是实例变量(static修饰符)、是否是常量(final修饰符)等。

字段计数器的值表示当前class文件fields表的成员个数。使用两个字节来表示。

字段表中每个成员都是一个field_info结构,用于表示该类或接口所声明的所有类字段或者实例字段,不包括方法内部声明的变量,也不包括从父类或父接口继承的那些字段。
字段表作为一个表,也有自己的结构:

类型 名称 含义 数量
u2 access_flags 访问标志 1
u2 name_index 字段名索引 1
u2 descriptor_index 描述符索引 1
u2 attributes_count 属性计数器 1
attribute_info attributes 属性集合 attributes_count
  • 访问标识
标志名称 标志值 描述
ACC_PUBLIC 0x0001 字段是否public
ACC_PRIVATE 0x0002 字段是否private
ACC_PROTECTED 0x0004 字段是否protected
ACC_STATIC 0x0008 字段是否static
ACC_FINAL 0x0010 字段是否final
ACC_VOLATILE 0x0040 字段是否volatile
ACC_TRANSIENT 0x0080 字段是否transient
ACC_SYNTHETIC 0x1000 字段是否由编译器自动生成
ACC_ENUM 0x4000 字段是否enum
  • 字段名索引
    根据字段名索引的值,查询常量池中的指定索引项。

  • 描述符索引
    描述符的作用是用来描述字段 的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类 型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大 写字符来表示,而对象类型则用字符L加对象的全限定名来表示。
    【深入理解java虚拟机】 - class类文件结构_第9张图片

  • 属性表集合
    属性表集合也包括属性计数器和属性集合。用于存储一些额外的信息,字段表可以在属性表中附加描述零至多项的额外信息。

字段表集合中不会列出从父类或者父接口中继承而来的字段,但有可能出现原本Java代码之中不存在的字段,如在内部类中为了保持对外部类的访问性,编译器就会自动添加指向外部类实例的字段。
另外,字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使 用不一样的名称,但是对于Class文件格式来讲,只要两个字段的描述符不是完全相同,那字段重名就 是合法的。

方法表集合

Class文件存储 格式中对方法的描述与对字段的描述采用了几乎完全一致的方式,方法表的结构如同字段表一样,依 次包括访问标志、名称索引、描述符索引、属性表集合几项。

方法表集合的访问标志如下:

标志名称 标志值 描述
ACC_PUBLIC 0x0001 方法是否public
ACC_PRIVATE 0x0002 方法是否private
ACC_PROTECTED 0x0004 方法是否protected
ACC_STATIC 0x0008 方法是否static
ACC_FINAL 0x0010 方法是否final
ACC_SYNCHRONIZED 0x0020 方法是否synchronized
ACC_BRIDGE 0x0040 方法是不是由编译器产生的桥接方法
ACC_VARAGES 0x0080 方法是否接受不定参数
ACC_NATIVE 0x0100 方法是否为本地方法
ACC_ABSTRACT 0x0400 方法是否public
ACC_STRICT 0x0800 方法是否private
ACC_SYNTHETIC 0x1000 方法是否protected

属性表集合

方法表集合之后的属性表集合,指的是class文件所携带的辅助信息,比如该class 文件的源文件的名称。以及任何带有RetentionPolicy.CLASS 或者RetentionPolicy.RUNTIME的注解。这类信息通常被用于Java虚拟机的验证和运行,以及Java程序的调试,一般无须深入了解。

此外,字段表、方法表都可以有自己的属性表。用于描述某些场景专有的信息。

属性表集合的限制没有那么严格,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,但Java虚拟机运行时会忽略掉它不认识的属性。

属性表集合的属性非常多,具体可以查看官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7

【深入理解java虚拟机】 - class类文件结构_第10张图片

你可能感兴趣的:(JVM,java,jvm,虚拟机,class,类文件结构)