JVM学习笔记(二)——Class文件结构

简介: Class文件是Java程序跨平台的保证,正是由于有了Class文件架起源码和机器码之间的中间桥梁,JVM虚拟机才可以在各种平台上按照统一的规范标准加载Java代码

Class文件是Java程序跨平台的保证,正是由于有了Class文件架起源码和机器码之间的中间桥梁,JVM虚拟机才可以在各种平台上按照统一的规范标准加载Java代码。

作为“写给虚拟机看的”Java代码,Class文件结构必须设计得足够完善,同时由于Java虚拟机规范并不只针对Java,Class文件又不能引入过多细节。本篇博客我们就来介绍下Class文件的结构。

一个Class文件对应一个Java Class,所以一个Class文件记录着一个类的全部信息,JVM通过Class文件将对应的类加载入内存。

Class文件的结构主要分为以下几部分:

  • 魔数
  • 常量池
  • 访问标识
  • 类索引、父类索引、接口索引
  • 字段表集合
  • 方法表集合
  • 索引表集合

1 魔数

每个Class文件的头4个字节成为魔数(Magic Number),它的唯一作用就是确定这个文件是否能作为一个Class文件被接受。很多文件都以魔数进行类型识别,如gif、jpeg等图片文件。之所以使用魔数而不是扩展名是处于安全考虑,文件扩展名可以所以改动。Class文件的魔数是0xCAFEBABE。

紧接着魔数的4个字节存储的是Class文件的版本号,5、6字节为次版本号,7、8字节为主版本号。不同版本的虚拟机可以接受不同版本的class文件,所以虚拟机通过主次版本号判断是否可以加载目标class文件。

2 常量池

常量池可以看做是Class文件的资源仓库,也是Class文件中占用空间最大的部分。常量池主要存放两大类常量:字面量、符号引用。

字面量比较接近Java语言层面的常量,如文本字符串、生命为final的常量等。

符号引用属于编译范畴中的概念,主要包括三类常量:

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

Java语言不同于C、C++等语言在编译阶段即进行链接,相应的链接都放到了运行时阶段。所以Class文件中不可能包含各个方法、字段在内存中的布局。Java虚拟机在运行阶段加载类时,将符号引用转换成真正的内存入口地址,对应类才算可以工作。

常量池中的每一项代表一个常量,JDK目前共有14中类型的常量,而每一个常量又有自己的内部结构。类或接口符号索引是其中较为简单的一项,接下来以类索引为例做简单介绍。类符号索引对应的类型为CONSTANT_Class_info,其结构如下:

类型 名称 数量
u1 tag 1
u2 name_index 1

tag是标志位,表明类型。CONSTANT_Class_info的tag为7。name_index是一个索引值,它指向常量池中一个CONSTANT_Utf8_info类型常量,此常量代表了这个类的全限定名。

CONSTANT_Utf8_info的结构如下所示:

类型 名称 数量
u1 tag 1
u2 length 1
u1 bytes length

bytes字段的内容就是类的全限定名。

3 访问标志

常量池之后的两个字节代表访问标志(accss_flags),用于识别类或接口的层次访问信息:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为public
ACC_FINAL 0x0010 是否被声明为final
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语义
ACC_INTERFACE 0x0200 是否为接口
ACCS_ABSTRACT 0x0400 是否为abstract类型
ACC_SYNTHETIC 0x1000 标示该类并非由用户代码产生
ACC_ANNOTATION 0x2000 标示这是一个注解
ACC_ENUM 0x4000 标示这是一个枚举

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

类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据集合。Class文件中的这三项决定了类的继承关系。

类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个CONSTANT_Class_info类描述符常量,通过CONSTANT_Class_info类型的常量索引值可以找到定义在CONSTAN_Utf8_info类型的常量中的类全限定名。

对于接口索引集合,入口的第一项u2类型的数据为接口计数器(interfaces_count)表示索引表的容量。每个接口的同样由一个u2类型数据指向一个CONSTANT_Class_info。

5 字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量和实例级变量,但不包括定义在方法内部的局部变量。每个字段的结构如下图所示:

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

5.1 访问标识

标志名称 含义
ACC_PUBLIC 是否为public
ACC_PRIVATE 是否为private
ACC_PROTECTED 是否为protected
ACC_STATIC 是否为static
ACC_FINAL 是否为final
ACC_VOLATILE 是否为volatile
ACC_TRANSIENT 是否为transient
ACC_SYNTHETIC 是否为编译器自动产生
ACC_ENUM 是否为enum

字段的访问标识access_flags与类访问标识类似。

5.2 name_index

name_index标识字段的简单名称。简单名称和全限定名的区别在于:全限定名是类的全路径名,如org/fenixsoft/clazz/TestClass,只是把类全名中的"."替换成“/”而已。简单名称指的是没有类型和参数修饰的方法或者字段名称,如一个类中含有一个字段"m",则其简单名称为"m"。

5.3 descriptor_index

descriptor_index为字段或方法的描述符。描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。基本数据类型以及代表无返回值的void以及对象类型均由一个大写字符来代替:

标志字段 含义
B byte
C char
D double
F float
I int
J long
S short
Z boolean
V void
L 对象类型,如Ljava/lang/object

对于数组类型,每一个维度用一个"["来描述,比如定义一个“java.lang.String[][]”类型的二维数组,将被记录为“[[Ljava/lang/string”。

方法描述符按照先参数列表后返回值的顺序描述,参数列表按照参数顺序放在一组"()"之内。如方法int indexOf(char[] source, int sourceOffest, int sourceCount, char[] target, int targetOffest, int targetCount, int fromIndex)的描述符为"([CII[CIII)I"。

5.4 attributes_count attribute_info

在描述符之后还有数量为attributes_count的attribute_info,attribute_info描述字段的额外信息,但这些额外信息最终存放在属性表中。如“final static int m = 123;”,那就可能会存在一项名称为ConstantValue的属性,其值指向常量123。

6 方法表集合

方法表和字段表结合几乎一样,理解了字段表,方法表就非常简单了。

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

由于volatile和transient不能修饰方法,所以方法表的访问标识中没有了ACC_VOLATILE,ACC_TRANSIENT标识。但同时又增加了代表synchronized native strictfp abstract的ACC_SYNCHRONIZED ACC_NATIVE ACC_STRICTFP ACC_ABSTRACT。

需要说明的是,方法表集合中并不包含方法里面的代码。方法代码经过编译后存放在方法属性集合中的一个名为"Code"的属性里面。例如某方法的属性表计数器attributes_count为1,则表示方法的属性表集合有一项属性,属性索引名称为0x0009,对应常量为code,说明此属性是方法的字节码描述。

7 属性表集合

Class文件、字段表、方法表都可以有自己的属性表,Java7里面定义了21种属性。

Code属性

并非所有方法表都有Code属性,比如接口和抽象类的方法就没有。结构如下:

类型 名称 数量 含义
u2 attribute_name_index 1 属性名的索引,对Code属性而言恒为”Code”
u4 attribute_length 1 属性值长度,相当于整个属性表长度长度减6(u2+u4)
u2 max_stack 1 操作数栈深度最大值。JVM运行时根据此值分配栈桢的操作栈深度
u2 max_locals 1 局部变量表所需存储空间,单位是Slot,double和long占用2个Slot、其他基本类型1Slot,Slot空间可以重用(变量作用域问题)
u4 code_length 1 编译后的字节码长度,理论上最长2^32-1,实际上JVM规定一个方法不允许超过65535条字节码指令
u1 code code_length 代码编译后的字节码
u2 exception_table_length 1 异常表长度
exception_info exception_table exception_table_length 异常表,记录字节码在start_pc到end_pc行之间如果出现类型为catch_type或其子类的异常则跳转到handler_pc行继续处理
u2 attibutes_count 1 属性表计数器
attribute_info attributes attibutes_count 属性额外描述,比如描述变量初始化值在常量池中的索引

字节码值得注意的一个地方是,javac编译时将this关键字作为一个普通方法参数由JVM调用时自动传入。

Exceptions属性

描述方法可能抛出的受检异常。

LineNumberTable属性

描述Java远吗行号与字节码行号之间映射关系,也就是为什么抛异常的时候可以显示源码哪一行抛出的。

LocalVariableTable属性

描述栈桢中局部变量表与Java源码中变量的关系,以保证编译后的代码被其他代码调用时,IDE可以显示参数名(否则被arg0、arg1之类的变量名代替)

SourceFile属性

描述生成当前Class文件的源文件名称,也是抛异常时可以显示源文件名字的原因。但内部类不会生成这个属性。

ConstantValue属性

static关键字修饰的变量可以使用这个属性。对于Sun javac编译器,final static的变量采用ConstantValue属性初始化,其他static变量在(类构造器)中初始化。

InnerClasses属性

记录内部类和宿主类的关联。内部类和宿主类的Class文件都会有这个属性。

Signature属性

记录泛型签名信息。Java的泛型是使用擦除式实现的伪泛型,编译后擦除泛型,这个属性为了弥补此缺陷,方便反射API可以拿到泛型类型。

相关文章
|
2月前
|
存储 算法 Java
聊聊jvm的内存结构, 以及各种结构的作用
【10月更文挑战第27天】JVM(Java虚拟机)的内存结构主要包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和运行时常量池。各部分协同工作,为Java程序提供高效稳定的内存管理和运行环境,确保程序的正常执行、数据存储和资源利用。
50 10
|
2月前
|
SQL 缓存 Java
JVM知识体系学习三:class文件初始化过程、硬件层数据一致性(硬件层)、缓存行、指令乱序执行问题、如何保证不乱序(volatile等)
这篇文章详细介绍了JVM中类文件的初始化过程、硬件层面的数据一致性问题、缓存行和伪共享、指令乱序执行问题,以及如何通过`volatile`关键字和`synchronized`关键字来保证数据的有序性和可见性。
31 3
|
2月前
|
小程序 Oracle Java
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
这篇文章是关于JVM基础知识的介绍,包括JVM的跨平台和跨语言特性、Class文件格式的详细解析,以及如何使用javap和jclasslib工具来分析Class文件。
49 0
JVM知识体系学习一:JVM了解基础、java编译后class文件的类结构详解,class分析工具 javap 和 jclasslib 的使用
|
4月前
|
存储 算法 Java
JVM组成结构详解:类加载、运行时数据区、执行引擎与垃圾收集器的协同工作
【8月更文挑战第25天】Java虚拟机(JVM)是Java平台的核心,它使Java程序能在任何支持JVM的平台上运行。JVM包含复杂的结构,如类加载子系统、运行时数据区、执行引擎、本地库接口和垃圾收集器。例如,当运行含有第三方库的程序时,类加载子系统会加载必要的.class文件;运行时数据区管理程序数据,如对象实例存储在堆中;执行引擎执行字节码;本地库接口允许Java调用本地应用程序;垃圾收集器则负责清理不再使用的对象,防止内存泄漏。这些组件协同工作,确保了Java程序的高效运行。
27 3
|
4月前
|
存储 安全 Java
JVM内存结构
这篇文章详细介绍了Java虚拟机(JVM)的内存结构,包括类的加载过程、类加载器的双亲委派机制、沙箱安全机制、程序计数器、Java栈、Java堆、本地方法和本地方法栈等关键组件及其作用。
JVM内存结构
|
4月前
|
C# UED 开发者
WPF动画大揭秘:掌握动画技巧,让你的界面动起来,告别枯燥与乏味!
【8月更文挑战第31天】在WPF应用开发中,动画能显著提升用户体验,使其更加生动有趣。本文将介绍WPF动画的基础知识和实现方法,包括平移、缩放、旋转等常见类型,并通过示例代码展示如何使用`DoubleAnimation`创建平移动画。此外,还将介绍动画触发器的使用,帮助开发者更好地控制动画效果,提升应用的吸引力。
196 0
|
5月前
|
Java
jmap 查看jvm内存大小并进行dump文件内存分析
jmap 查看jvm内存大小并进行dump文件内存分析
100 3
|
4月前
|
存储 监控 算法
深入解析JVM内部结构及GC机制的实战应用
深入解析JVM内部结构及GC机制的实战应用
|
5月前
|
存储 运维 Java
Java面试题:JVM的内存结构有哪些主要部分?请简述每个部分的作用
Java面试题:JVM的内存结构有哪些主要部分?请简述每个部分的作用
60 9
|
5月前
|
存储 设计模式 监控
Java面试题:简述JVM的内存结构,包括堆、栈、方法区等。栈内存优化的方法有 哪些?
Java面试题:简述JVM的内存结构,包括堆、栈、方法区等。栈内存优化的方法有 哪些?
48 0