文章目录


Class 文件结构细节

· 官方文档位置:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html">​https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html​

· Class 类的本质

任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,Class文件实际上它并不一定以磁盘文件的形式存在。Class 文件是一组以8位字节为基础单位的二进制流。

· Class 文件格式

Class 的结构不像 XML 等描述语言,由于它没有任何分隔符号。所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。

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

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

· 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件本质上就是一张表。 由于表没有固定长度,所以通常会在其前面加上个数说明

· 代码举例

public class Demo {
private int num = 1;

public int add(){
num = num + 2;
return num;
}
}

对应的字节码文件:

Class 文件结构(一)_java字节码

换句话说,充分理解了每一个字节码文件的细节,自己也可以反编译出Java源文件来。

class文件结构细节概述

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

Class文件的总体结构如下:(百度面试题)

-  魔数

- Class文件版本

- 常量池

- 访问标识(或标志)

- 类索引,父类索引,接口索引集合

- 字段表集合

- 方法表集合

- 属性表集合

Class 文件结构(一)_字面量_02

Class 文件结构(一)_字面量_03

这是一张Java字节码总的结构表,我们按照上面的顺序逐一进行解读就可以了

class文件的魔数是什么?

Magic Number(魔数):class文件的标志

· 每个 Class 文件开头的4个字节的无符号整数称为魔数(Magic Number)

· 它的唯一作用是确定这个文件是否为一个能被虚拟机接受的有效合法的Class文件。即:魔数是Class文件的标识符。

· 魔数值固定为0xCAFEBABE。不会改变。

· 如果一个Class文件不以0xCAFEBABE开头,虚拟机在进行文件校验的时候就会直接抛出以下错误:

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 class file StringTest

· 使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。

如何确保高版本的JVM可执行低版本的class文件?

· 不同版本的Java编译器编译的Class文件对应的版本是不一样的。目前,高版本的Java虚拟机可以执行由低版本编译器生成的Class文件,但是低版本的Java虚拟机不能执行由高版本编译器生成的Class文件。否则JVM会抛出java.lang.UnsupportedClassVersionError异常。 (向下兼容)

· 在实际应用中,由于开发环境和生产环境的不同,可能会导致该问题的发生。因此,需要我们在开发时,特别注意开发编译的JDK版本和生产环境中的JDK版本是否一致。

class文件版本号

· 紧接着魔数的 4 个字节存储的是 Class 文件的版本号。同样也是4个字节。第5个和第6个字节所代表的含义就是编译的副版本号minor_version,而第7个和第8个字节就是编译的主版本号major_version。

· 它们共同构成了class文件的格式版本号。譬如某个 Class 文件的主版本号为 M,副版本号为 m,那么这个Class 文件的格式版本号就确定为 M.m。

· 版本号和Java编译器的对应关系如下表:

Class 文件结构(一)_java字节码_04

· Java 的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1。

· 虚拟机JDK版本为1.k (k >= 2)时,对应的class文件格式版本号的范围为45.0 - 44+k.0 (含两端)。

常量池:class文件的基石?作用是?

常量池:存放所有常量

· 常量池是Class文件中内容最为丰富的区域之一。常量池对于Class文件中的字段和方法解析也有着至关重要的作用。

· 常量池:可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型(后面的很多数据类型都会指向此处),也是占用Class文件空间最大的数据项目之一。

· 常量池表项中,用于存放编译时期生成的各种字面量符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放。

Class 文件结构(一)_编译器_05

· 在版本号之后,紧跟着的是常量池的数量,以及若干个常量池表项。

为什么需要常量池计数器?

constant_pool_count (常量池计数器)

· 由于常量池的数量不固定,时长时短,所以需要放置两个字节来表示常量池容量计数值。

· 常量池容量计数值(u2类型):1开始,表示常量池中有多少项常量。即constant_pool_count=1表示常量池中有0个常量项。

· Demo的值为:

Class 文件结构(一)_常量池_06

其值为0x0016,掐指一算,也就是22。

需要注意的是,这实际上只有21项常量。索引为范围是1-21。为什么呢?


通常我们写代码时都是从0开始的,但是这里的常量池却是从1开始,因为它把第0项常量空出来了。这是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可用索引值0来表示。


常量池表

constant_pool [ ](常量池)

· constant_pool是一种表结构,以 1 ~ constant_pool_count - 1为索引。表明了后面有多少个常量项。

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

它包含了class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其他常量。常量池中的每一项都具备相同的特征。第1个字节作为类型标记,用于确定该项的格式,这个字节称为tag byte (标记字节、标签字节)。

Class 文件结构(一)_java字节码_07

2.1 字面量和符号引用

在对这些常量解读前,我们需要搞清楚几个概念。

什么是字面量



在计算机科学中,字面量(literal)是用于表达源代码中一个固定值的表示法(notation)。几乎所有计算机编程语言都具有对基本值的字面量表示,诸如:整数、浮点数以及字符串;而有很多也对布尔类型和字符类型的值也支持字面量表示;还有一些甚至对枚举类型的元素以及像数组、记录和对象等复合类型的值也支持字面量表示法。


以上是关于计算机科学中关于字面量的解释,并不是很容易理解。说简单点,字面量就是指由字母、数字等构成的字符串或者数值。

字面量只可以右值出现,所谓右值是指等号右边的值,如:int a=123这里的a为左值,123为右值。在这个例子中123就是字面量。

int

a

=


123;


String

s

=


"hollis";


上面的代码事例中,123和hollis都是字面量。


什么是符号引用


常量池中,除了字面量以外,还有符号引用,那么到底什么是符号引用呢。

符号引用是编译原理中的概念,是相对于直接引用来说的。主要包括了以下三类常量:


  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

这也就可以印证前面的常量池中还包含一些​com/hollis/HelloWorld​​​、​​main​​​、​​([Ljava/lang/String;)V​​等常量的原因了。


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

Class 文件结构(一)_java字节码_08

2.1.1 全限定名

com/zhuyuan/test/Demo这个就是类的全限定名,仅仅是把包名的".“替换成”/",为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束。

2.1.2 简单名称

简单名称是指没有类型和参数修饰的方法或者字段名称,上面例子中的类的add()方法和num字段的简单名称分别是add和num。

2.1.3 描述符

**描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。**根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示,详见下表: (数据类型:基本数据类型 、 引用数据类型)Class 文件结构(一)_编译器_09

用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如:

方法java.lang.String toString()的描述符为() Ljava/lang/String;,

方法int abc(int[] x, int y)的描述符为([II) I。

· 谈谈你对符号引用、直接引用的理解?


这里说明下符号引用和直接引用的区别与关联:

· 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。

· 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。


常量类型和结构

常量池中每一项常量都是一个表,JDK1.7之后共有14种不同的表结构数据。如下表格所示:

Class 文件结构(一)_java_10

总结:

· 这14种表(或者常量项结构)的共同点是:表开始的第一位是一个u1类型的标志位(tag),代表当前这个常量项使用的是哪种表结构,即哪种常量类型。

· 在常量池列表中,CONSTANT_Utf8_info常量项是一种使用改进过的UTF-8编码格式来存储诸如文字字符串、类或者接口的全限定名、字段或者方法的简单名称以及描述符等常量字符串信息。

· 这14种常量项结构还有一个特点是,其中13个常量项占用的字节固定,只有CONSTANT_Utf8_info占用字节不固定,其大小由length决定。为什么呢?因为从常量池存放的内容可知,其存放的是字面量和符号引用,最终这些内容都会是一个字符串,这些字符串的大小是在编写程序时才确定,比如你定义一个类,类名可以取长取短,所以在没编译前,大小不固定,编译后,通过utf-8编码,就可以知道其长度。

访问标识

访问标识(access_flag、访问标志、访问标记)

· 在常量池后,紧跟着访问标记。该标记使用两个字节表示,用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被声明为 final 等。各种访问标记如下所示:

Class 文件结构(一)_常量池_11

· 类的访问权限通常为 ACC_ 开头的常量。

· 每一种类型的表示都是通过设置访问标记的32位中的特定位来实现的。比如,若是public final的类,则该标记为ACC_PUBLIC | ACC_FINAL。

· 使用ACC_SUPER可以让类更准确地定位到父类的方法super.method(),现代编译器都会设置并且使用这个标记。