深入理解JVM读书笔记: Class类文件结构

Class文件是一组以8位字节为基础单位的二进制流。采用一种类似于C语言结构体的微结构来存储数据,只有两种数据类型:无符号数和表。其中无符号数数据基本的数据类型,以u1、u2、u4、u8表示1、2、4、8字节的无符号数,用于描述数字、索引引用、数量值或者UTF-8编码字符串;表则是由无符号树和其他表的复合数据类型,以_info后缀。整个Class文件本质上就是一张表:

解析Class文件各个数据项含义:

魔数

头4个字节为魔数Magic Number,唯一作用是识别文件是否能被虚拟机接受。

版本号

紧接着后4个字节为Class文件版本号,5、6位此版本号,7、8位主版本号,如下:

常量池

主次版本号之后是常量池constant_pool入口,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据。主要存放两大类常量:字面量和符号引用。字面量如文本字符串、Final常量等;符号引用则包括下面三类常量:

a.类和接口的全限定名

b.字段的名称和描述符

c.方法的名称和描述符

因为Java代码在Javac编译的时候没有“连接”步骤,需要在虚拟机加载Class文件是动态连接,所以Class文件不会保存最终内存布局信息,需要从常量池中获取对应符号引用,再在类创建时或运行时解析、翻译到具体内存地址之中(类创建和动态连接内容,见下一节)

这里个人理解符号引用的作用就是在编译时记录下文件的类、字段和方法,在JVM运行时能在需要的时候获取相应信息进行加载。

常量池中每一项都是一个表,这每个表开始第一位是一个u1类型标志位,含义如下:

其中:

length表示UTF-8编码字符串字节长度,后面是长度为length的UTF-8略缩码(UTF-8缩略编码与普通UTF-8编码 的区别是:从‘\u0001‘到‘\u007f‘之间的字符(相当于1~127的ASCII码)的缩略编码使用一个 字节表示,从‘\u0080‘到‘\u07ff‘之间的所有字符的缩略编码用两个字节表示,从‘\u0800‘到‘\uffff‘之间的所有字符的缩略编码就按照普通UTF-8编码规则使用三个字节表示。)

这里length为u2表示如果顶一个超过64KB引文字符的变量或方法名,将会无法编译,另外,Class文件中方法、字段等都需要引用Utf8_info型常量来描述名称。

下图表示常量池中所有常量项结构表

访问标志(类)

常量池之后的2个字节为访问标志(access_flags):

类索引、父类索引、接口索引

类索引this_class、父类索引super_class、都是u2类型数据,而接口索引interfaces集合是一组u2类型数据集合,指向常量池中CLass_info的符号引用,由着三项数据确定类的继承关系。

字段表

字段表用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量,结构如下:

其中access_flags与类中的access_flags相似,name_index、descriptor_index都是对常量池的引用,表示字段简单名称和方法描述符。

这里讲下描述符规则,首先,基本数据类型以及void类型都用一个大写字符表示,对象类型使用L加对象全限定名表示:如图

而对于数组类型,每一位度将使用一个前置的"["字符来描述,“[[Ljava/lang/String;”表示String[][]。

在描述方法时采用先参数列表后返回值顺序描述,"([CII[CIII)I"来描述int indexOf(char[],int ,int ,char[] ,int ,int ,int )"

attributes则表示额外的描述信息。如声明"final static int m = 123"则可能存在ConstantValue的属性指向向量123.

这里注意,字段表集合中不会列出从超类或者父接口中继承而来的字段,但可能列出Java代码中不存在的字段,如内部类中保持对外部类的访问性,会自动添加指向外部类实例的字段;

接口向上声明,继承向下声明(接口与继承的区别)

方法表

对方法表的描述与对字段的描述几乎采用了完全一致的方式,其中表的结构与字段表一样,仅在访问标志和属性表集合的可选项有所区别。

这里,方法里面的代码经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为Code的属性里面,后面会讲到。

同样,如果父类方法在子类中没有被重写,那么方法表集合中不会出现来自父类的方法;同时也同样可能出现编译器自动添加的方法,典型如“类构造器<clinit>”和实例构造器"<init>",后面讲。

如果要重载一个方法除了要与原方法具有相同的简单名称之外,还必须要求拥有一个与原方法不同的特征签名,即方法中各个参数在常量池中的字段符号引用集合,不包含返回值。这就是Java语言里仅仅依靠返回值不同无法对一个已有方法重载。但是在Class文件格式中即字节码层面(前面是Java代码层面),方法特征还包括方法返回值及受查异常表,因此2个不是完全一致的方法也可以合法共存与一个Class文件中。

属性表

属性表(attribute_info)在前面出现很多次,Class文件、字段表、方法表都可以携带自己的属性表集合,用于描述专有信息。

属性表集合限制稍微宽松一些,目前属性表实现的能识别的属性如下:

其中,属性名要从常量池引用utf8_info表示,而属性值的结构是自定义的结构如下:

code属性

程序方法体重的代码在编译后变为字节码放在Code属性内。Code属性出现在方法表的属性集合中,如果方法表有Code属性,则其结构:

前两个不讨论,

max_stack表示操作数栈(Operand Stacks)深度的最大值。方法执行的任意时刻操作数栈都不会超过深度。虚拟机运行时需要根据它开分配栈帧(Stack Frame)的操作栈深度;

max_locals代表了局部变量表所需的存储空间,单位是slot,slot是虚拟机为局部变量

分配内存所使用的最小单位。对不超过32位的数据类型占用1个slot,double、long这两种64位的数据类型则需要两个slot。局部变量表中的slot可以被复用。

code_length和code用来存储编译后生成的字节码指令,这里code是u1则表示8位256,所以可以表达256条指令。虚拟机规范已经定义了其中约200条编码指令对应。另外关于code_length虽然是u4类型长度值,但虚拟机规范限制一个方法不允许超过65535条字节码指令,即实际上只使用了u2长度,超过了编译器会拒绝。

code属性Class文件中最重要的一个属性,下面详解code:

这里没定义任何局部变量,也没传任何参数,Locals=1,Args_size=1是因为this,任何实例方法里都可以使用this指向方法所述对象,这实际上是把this关键字转变为一个普通方法参数的访问,在虚拟机调用实例方法是自动传入了。局部变量表也会留一个slot来存储this引用。

那么改成static呢?个人感觉是Args_size和Locals都为0了。

code之后是显式异常处理表,这个不是必须存在的:

这些字段表示为当字节码在start_pc行到end_pc(不包括)出现了类型为catch_type或者其子类的异常(catch_type指向一个Class_info型的常量索引),则跳转到handler_pc行继续处理。catch_type为0则任意情况都需要转向handler_pc进行处理。

Exception属性

方法表中与Code属性平级的属性。作用是列举方法中可能抛出的受查异常,结构如下:

方法可能抛出number_of_exceptions种受查异常,每一种使用一个exception_index_table项表示,指向Class_info型的常量索引

LineNumberTable属性

用于描述Java源码行号与字节码行号之间的对应关系,非运行必须属性,如图:

line_number_table为line_number_info的集合,包括start_pc和line_number两个u2,前者是字节码行号,后者是Java源码行号。

LocalVariableTable属性

用于描述栈帧中局部变量表中的变量与Java源码中变量之间关系,非运行必须属性。

Local_variable_info项目代表了一个栈帧与源码的局部变量关联,结构如下:

前二者表示字节码偏移量及范围覆盖长度,后面两个为Utf8_info索引,代表局部变量名称和描述符,index是这个局部变量在栈帧局部变量表中Slot的位置。

SourceFile属性

记录生成Class文件的源码文件名称。可选,大多数类名和文件名一致,但也有例外

其中sourcefile是utf8_info型常量索引,常量值时源码文件的文件名。

ConstantValue属性

作用是通知虚拟机自动为静态变量赋值。对于非static类型变量在实例构造器<init>中进行;对于static关键字修饰的变量,目前Sun Javac编译器选择为:如果同时使用final static且数据类型为基本类型或者java.lang.String就生成ConstantValue属性来初始化,否则会选择在类构造器<clinit>方法中进行初始化。ConstantValue只能限于基本类型和String,因为属性的属性值只是一个常量池的索引号,所以无法支持别的类型。结构如下:

这里看到ConstantValue为定长属性,所以它的attribute_length必须固定为2,constantvalue_index代表常量池中一个字面量常量的引用。

InnerClasses属性

用于记录内部类与宿主类之间关联,如果一个类中定义了内部类,那编译器将会为它以及它所包含的内部类生成InnerClasses属性,结构如图:

number_of_classes代表需要记录多少个内部类信息,每一个内部类的信息由inner_classes_info描述:如图

前两个指向Class_info常量索引,代表内部类和宿主类的符号引用,inner_name_index代表内部类名称,匿名类则为0。inner_class_access_flags是内部类的访问标志,类似于类的access_flags

Deprecated及Synthetic属性

这两个属性只有有和没有的区别没有属性值概念,Deprecated表示某个类、字段、方法不再被推荐使用(注解)

Synthetic属性代表此字段或方法并不是由Java源码直接产生的,而是由编译器自行添加的,也可以设置他们访问标志中ACC_SYNTHETIC标志位,所有非用户代码产生的类、方法、字段都应当至少设置Synthetic属性或ACC_SYNTHETIC,除了实例构造器和类构造器。

StackMapTable属性

它是一个复杂变长属性,位于Code属性的属性表中。会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流分析的类型推到验证器。(暂不多深究,待需要时研究)

Signature属性

定长属性,可以出现于类、字段表和方法表结构的属性表中,JDK1.5后任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则Signure属性会为它记录泛型签名信息。专门使用这样一个属性记录泛型类型是因为Java语言的泛型采用的是擦除发实现的(即在使用泛型是无法获取泛型的类型信息),在字节码(code属性)中,泛型信息编译之后都统统被擦除掉,好处是实现简单、运行期能节省一些类型所占的内存空间。坏处是无法将泛型类型与用户定义普通类型同等对待。因此Signature专门为了弥补这个缺陷,Java反射Api能够获取泛型,最终的数据来源就是这个属性。

这里Parameterized Types指的是参数化的类型,如ArrayList<Integer>、ArrayList<E>、ArrayList<? extends Number>等;而Type Variable指的是类型变量,如ArrayList<E>中的E。

Signature结构如下:

其中signature_index值必须是一个对常量池的有效索引,且必须是utf8_info结构,表示类签名、方法类型签名或字段类型签名,这取决于当前Signature属性是哪个表的属性。

BootstrapMethods属性

复杂的边长属性,位于类文件的属性表中。用于保存invokedynamic指令引用的引导方法限定符。如果某个类文件结构的常量池中曾经出现过InvokeDynamic_info类型常量,那么这个类文件中的属性表必须存在一个明确的BootstrapMethods属性。另外即使常量池中出现多次InvokeDynamic_info,属性表中也最多只有一个BootstrapMethods属性。该属性与InvokeDynamic执行和java.lang.Invoke包关系密切,后面讲。

结构如下:

bootstrap_method如下:

bootstrap_method数组每个成员包含了一个指向常量池MethodHandle结构的索引值,它代表一个引导方法,还包含方法静态参数序列。其中bootstrap_method_ref为指向MethodHandle_info的有效索引、bootstrap_arguments为参数变量,必须是常量池的下列结构之一:String、Class、Integer、Long、Float、Double、MethodHandle、MethodType

字节码指令简介

由于虚拟机采用面向操作数栈而不是寄存器的架构,所以大多数的指令都不包含操作数,只有一个操作码,由于限制了操作码长度为1个字节(即0~255),劣势是操作码总数有限制,且因为编译时不对齐操作数长度,因此去超过1个字节数据时,就必须重建具体的数据结构。如16长度无符号数表示:

(byte1 << 8) | byte2

坏处是解释执行字节码时会损失一些性能,但优势是带来了高传输效率。

如果不考虑异常处理,那么虚拟机的解释器可以使用下面的模型来理解:

字节码与数据模型

由于操作码只有一个字节长度,所以并非每一种数据类型和每一种操作都有对应的指令。有一些单独的指令可以在必要的时候用来将一些不支持的类型转换为可被支持的类型。

如图:列举了JVM所支持与数据类型相关的字节码指令,通过数据类型列所代表的特殊字符替换opcode列的指令模板中的T,可以得到一个具体的字节码指令,若为空则表示JVM不支持这种数据类型执行这项操作。

可以看出大多数没有支持整数类型byte、char、short甚至、boolean,编译器会在编译或运行时将这些类型扩展为相应的int类型。

这里讲字节码操作按用途大致分为9类:加载和存储、运算指令、类型转换、对象创建与访问、操作数栈管理、控制转移、方法调用和返回、异常处理、同步。这里简述,详细需要另外查阅规范

加载与存储:

Tload将一个局部变量加载到操作栈。

Tstore将一个数从操作数栈存储到局部变量表。

Tconst将常量加载到操作数栈

wide扩充局部变量表的访问索引。

运算:

Tadd加、Tsub减、Tmul乘、Tdiv除、Trem求余、Tneg取反、Tshl|r位移、Tor或、Tand与、Txor异或、iinc自增、Tcmpg比较。

类型转化:

用于解决用户代码中显示类型转换或前面所提到字节码指令集中数据类型指令无法与数据类型一一对应的问题。

小转大稳安全转换,无需指令;大转小需要指令如i2b,i2c,i2s,l2i,f2i,f2l,d2i,d2l,d2f。

浮点转整数规则:

1.浮点为NaN时,结果为0.

2.浮点非无穷大则向零舍入取整,且在整形表示范围之内,则为该数

3.否则转为整形表示最大或最小正数。

对象创建与访问指令

new创建类实例指令

newarray、anewarray、multianewarray创建数组指令

访问类字段和实例字段getfield、putfield、getstatic、putstatic

加载数组元素到操作数栈Taload

从操作数栈的值存储到数组元素中Tastore

取数组长度arraylength

检查类实例类型instanceof、checkcast

操作数栈管理指令

将栈顶1或2个元素出栈pop、pop2

复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2

将栈最顶端的两个数值互换:swap

控制转移指令

这里简述,大致可以理解为有条件或无条件地修改PC寄存器的值。

方法调用和返回指令

将在后面具体讲解:

invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派,最常见。

invokeinterface调用接口方法,运行时搜索一个实现这个接口方法的对象,找出适合的方法调用

invokespecial调用需要特殊处理的实例方法,包括实例初始化、私有方法和父类方法

invokestatic调用static方法

invokedynamic在运行时动态解析出调用点限定符所引用的方法,前4个是固化在jvav虚拟机内部,这个逻辑则是用户所设定的引导方法。

调用指令与数据类型无关,返回指令根据返回值类型区分,Xreturn以及return 供void、实例初始化以及类和接口的类初始化方法使用。

异常处理命令

athrow显式抛出异常。

这里catch处理异常使用异常表来完成而不是字节码指令实现的。

同步指令

JVM可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构是使用管程(monitor)来支持的。

方法级的同步是隐式的,无需通过字节码指令来控制。虚拟机可以从ACC_SYNCHRONIZED访问标志判断是否为同步方法。

当方法调用时,调用指令先检查访问标志,然后执行线程会要求先持有管程,再执行方法,完成后释放管程。当然期间其他任何线程都无法再获取同一个管程。如果一个同步方法执行期间抛出异常且内部无法处理异常,那么方法所持有的管程将在异常抛到同步方法之外是自动释放。

JVM指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字。

编译器保证无论通过何种方式完成、每条monitorenter都必须执行对应的monitorexit,无论是正常结束还是异常结束。

公有设计和私有实现

这一段我的理解大致是,Class文件设计的目的在于定义了结构,与底层硬件、系统及虚拟机的具体实现方式区别开,保证在不同平台上使用使用统一Class字节码文件规范实现各自不同虚拟机,并且鼓励虚拟机在遵循规范的前提下进行内部实现的优化。

虚拟机实现的方式主要有以下两种:

将输入的Java虚拟机代码在加载或执行时翻译成另外一种虚拟机的指令集。

将输入的Java虚拟机代码在加载或执行时翻译成宿主机CPU的本地指令集(即JIT代码生 成技术)。

时间: 2024-12-25 18:43:46

深入理解JVM读书笔记: Class类文件结构的相关文章

java内存区域——深入理解JVM读书笔记

本内容由<深入理解java虚拟机>的部分读书笔记整理而成,本读者计划连载. 通过如下图和文字介绍来了解几个运行时数据区的概念. 方法区:它是各个线程共享的区域,用于内存已被VM加载的类信息.常量.静态变量.即时编译器编译的代码等数据.JVM规范对这个区域的限制很宽松,如同堆一样不需要连续的内存.可选择固定大小.可扩展的大小外,还可以选择不实现垃圾收集.因为在些区域的垃圾收集必要性不高且效果较差.如果回收也是常量池的回收和类型的卸载,但此操作异常困难.当方法区无法满足内存的分配时,抛OutOfM

深入理解JVM读书笔记四: (早期)编译器优化

10.1概述 Java 语言的 "编译期" 其实是一段 "不确定" 的操作过程,因为它可能是指一个前端编译器(其实叫 "编译器的前端" 更准确一些)把 .java 文件转变成 .class 文件的过程:也可能是指虚拟机的后端运行期编译器(JIT 编译器,Just In Time Compiler)把字节码转变成机器码的过程:还可能是指使用静态提前编译器(AOT 编译器,Ahead Of Time Compiler)直接把 *.java 文件编译成

深入理解JVM读书笔记三: 虚拟机类加载机制

Java虚拟机类加载机制是把Class类文件加载到内存,并对Class文件中的数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的java类型的过程. 7.1概述 与那些在编译时需要进行链接工作的语言不同,在Java语言里面,类型的加载和链接过程都是在程序运行期间完成的(其实C++也是分为静态链接库和动态链接库的),这样会在类加载时稍微增加一些性能开销,但是却能为Java应用程序提供高度的灵活性,Java中天生可以动态扩展的语言特性就是依赖运行期动态加载和动态链接这个特点实现的. 7.

深入理解JVM读书笔记二: 垃圾收集器与内存分配策略

3.2对象已死吗? 3.2.1 引用计数法 给对象添加一个引用计数器,每当有一个地方引用它的地方,计数器值+1:当引用失效,计数器值就减1;任何时候计数器为0,对象就不可能再被引用了. 它很难解决对象之间相互循环引用的问题. 3.2.2 可达性分析算法 这个算法的基本思路就是通过一系列的称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC

深入理解JVM读书笔记一: Java内存区域与内存溢出异常

Java虚拟机管理的内存包括几个运行时数据内存:方法区.虚拟机栈.本地方法栈.堆.程序计数器,其中方法区和堆是由线程共享的数据区,其他几个是线程隔离的数据区. 2.2 运行时数据区域 2.2.1程序计数器 程序计数器是一块较小的内存,他可以看做是当前线程所执行的行号指示器.字节码解释器工作的时候就是通过改变这个计数器的值来选取下一条需要执行的字节码的指令,分支.循环.跳转.异常处理.线程恢复等基础功能都需要依赖这个计数器来完成.如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚

深入理解JVM读书笔记五: Java内存模型与Volatile关键字

12.2硬件的效率与一致性 由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了. 基于高速缓存的存储交互很好地理解了处理器与内存的速度矛盾,但是也为计算机系统带来了更高的复杂度,因为它引入了一个新的问题: 缓存一致性(Cache Coherenc

深入理解Java虚拟机笔记---class类文件结构概述

class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格紧凑地排列在class文件中,中间没有任何分隔符.当遇到需要占用8位字节以上的的数据项时,则会按照高位在前的方式侵害成若干个8位字节进行存储. 根据Java虚拟机规范的规定,class文件格式采用一种类似于C语言结构体的伪结构来存储,这种伪结构只有两种数据类型:无符号数和表.无符号数属于基于数据类型,以u1.u2.u4.u8来分别代码1个字节.2个字节.4个字节.8个字节的无符号数,无符号数可以用于描述数字.索引引用.数量值,或

深入理解计算机系统读书笔记一 ---&gt; 计算机基础漫游

一.程序编译的不同阶段. 通常我们是以高级程序开发易于阅读的代码,我们通过语法规则推断代码的具体含义.但是计算机执行代码的时候就需要把代码解析成既定的可执行问题,计算机是如何处理的呢?这里以C语言hello.c文件为例来说明中间过程. #include <stdio.h> int main() { printf("hello world!\n"); } 先上张图. C语言源程序----预处理解析头文件和函数  --- 编译器解析成汇编语言 ---   翻译机器语言指令,打包

深入JVM读书笔记(一)——jvm数据区基础知识

最近得空,就把<深入理解Java虚拟机>重新看了一遍,特写下现在的读书笔记,总结知识点,记录现在的理解,便于以后的回顾.下面的内容也会按照这本书的章节来划分知识点! Let's go! 想要了解Java虚拟机,一定要先明白Java运行时划分为哪些数据区域,具体的可以参考下图,按照是否为线程私有可以划分为: 线程私有:虚拟机栈.本地方法栈.程序计数器 线程共有:方法区.堆   下面详细说一下各个数据区的作用: 1. 程序计数器(Program Counter Register) 程序计数器是一块