1. 概述

计算机只认识0和1,我们编写的程序需要经编译器翻译为由0和1构成的二进制文件才能被计算机执行。伴随着虚拟机和大量建立在虚拟机上程序语言的出现,将程序编译为本地字节码文件已不再是唯一的选择,越来越多的程序语言选择了与操作系统无关的,平台中立的格式作为程序编译后的存储格式。

2. 无关性

虚拟机提供商发布了许多可以运行在各种不同平台上的虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,实现了程序的"一次编写,到处运行"。各个不同平台的虚拟机与所有平台都统一使用相同的程序存储格式——字节码,它是构成平台无关性的基石。

Java虚拟机还支持语言无关性,它同时支持Java,Clojure,Groovy,JRuby,Jython,Scala等,实现语言无关性的基础仍然是虚拟机和字节码存储格式。Java虚拟机不和包含Java在内的任何语言进行绑定,它只与"class文件“这种特定的二进制文件格式关联,class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。基于安全方面的考虑,Java虚拟机规范要求在class文件中使用许多强制性的语法和结构化约束。Java虚拟机支持的不同的语言通过不同的编译器都可以翻译为字节码文件,最终在Java虚拟机上运行。如下图:

3. class文件结构

注意:任何一个class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以通过类加载器直接生成)

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

Big-Endian:高位在前存储方式,高位字节放在内存的低地址端,低位字节放在内存的高地址端

Little-Endian:低位在前存储方式,高位字节放在内存的高地址端,低位字节放在内存的低地址端

据Java虚拟机规范的规定,class文件格式采用类似c语言结构体的伪结构来存储数据,伪结构中只有两种数据类型无符号数和表,后面的解析都要以这两种数据类型为基础。

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

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

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

3.1 魔数

每个class文件的头4个字节称为魔数,它的唯一作用是确定这个文件是否为一个能被虚拟机接受的class文件。许多文件存储标准中都使用魔数来进行身份的识别,譬如图片格式,如gif或者jpeg等在文件头中都存在有魔数。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。文件的制定者可以自由地选择魔数值,只要这个魔数值还没有被广泛采用过同时又不会引起混淆即可。class文件的魔数值为:0xCAFEBABE

紧接着魔数的4个字节存储的是class文件的版本号:第5个和第6个字节是次版本号,第7个和第8个字节是主版本号,Java的版本号是从45开始的。JDK1.1之后的每个JDK大版本发布主版本号向上加1,高版本的JDK能向下兼容以前版本的class文件,但不能执行以后版本的class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的class文件。如下代码:

package com.test;
public class Test{public static void main(String[] args){System.out.println("Hello World");}
}

用WinHex打开对应class文件结果:

其中开头4个字节的十六进制表示的是0xCAFEBABE,代表此版本号的第5个和第6个字节值为0x0000,而主版本号的值为0x0034,也即是十进制的54

3.2 常量池

紧接着主次版本号;class文件之中的资源仓库;与其他项目关联最多的数据类型;占用class文件空间最大的数据项目之一;class文件中第一个出现的表类型数据项目。入口放置一项u2类型的数据,代表常量池计数值,从1开始;第0项空出来原因,满足后面某些指向常量池的索引值的数据在特定情况下需要表达"不引用任何一个常量池项目",这种情况可以把索引值置为0。class文件结构中只有常量池的容量计算是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等容量计算都从0开始。

常量池中主要放两大类常量:字面量和符号引用。字面量接近于Java语言的常量概念,如文本字符串,final修饰的常量值等。符号引用包括三类常量:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。

在class文件中不会保存各个方法、字段的最终内存布局信息,这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时进行解析、翻译到具体的内存地址之中。

常量池的项目类型
类型 标志 描述
CONSTANT_Utf8_info 1 UTF-8编码的字符串
CONSTANT_Ingeter_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_InvokeDynamic_info 18 表示一个动态方法调用点
常量池中的14种常量项的结构总表
常量 项目 类型 描述
CONSTANT_Utf8_info tag u1 值为1
length u2 UTF-8编码的字符串占用的字节数
bytes u1 长度为length的UTF-8编码的字符串
CONSTANT_Ingeter_info tag u1 值为3
bytes u4 按照高位在前存储的int值
CONSTANT_Float_info tag u1 值为4
bytes u4 按照高位在前存储的float值
CONSTANT_Long_info tag u1 值为5
bytes u8 按照高位在前存储的long值
CONSTANT_Double_info tag u1 值为6
bytes u8 按照高位在前存储的double值
CONSTANT_Class_info tag u1 值为7
index u2 指向全限定名常量项的索引
CONSTANT_String_info tag u1 值为8
index u2 指向字符串字面量项的索引
CONSTANT_Fieldref_info tag u1 值为9
index u2 指向声明字段的类或接口描述符CONSTANT_Class_info的索引项
index u2 指向声明字段描述符CONSTANT_NameAndType_info的索引项
CONSTANT_Methodref_info tag u1 值为10
index u2 指向声明方法的类描述符CONSTANT_Class_info的索引项
index u2 指向名称及类型描述符CONSTANT_NameAndType_info的索引项
CONSTANT_InterfaceMethodref_info tag u1 值为11
index u2 指向声明方法的类描述符CONSTANT_Class_info的索引项
index u2

指向名称及类型描述符CONSTANT_NameAndType_info的索引项

CONSTANT_NameAndType_info tag u1 值为12
index u2 指向该字段或方法名称常量项的索引
index u2

指向该字段或方法描述符常量项的索引

CONSTANT_MethodHandle_info reference_kind u1 值必须在1~9之间(包括1和9),它决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为
reference_index u2 值必须是对常量池的有效索引
CONSTANT_MethodType_info tag u1 值为16
descriptor_index u2 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符
CONSTANT_NameAndType_info tag u1 值为18
bootstrap_method_attr_index u2 值必须是对当前class文件中引导方法表的bootstrap_methods[]数值的有效索引
name_and_type_index u2 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符

使用命令 javap -verbose com/test/Test.class 输出Test.class文件的字节码内容

Classfile /E:/com/test/Test.classLast modified 2018-7-31; size 422 bytesMD5 checksum 2307b9cf744b10b89d0f30adb62d1f5dCompiled from "Test.java"
public class com.test.Testminor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER
Constant pool:#1 = Methodref          #6.#15         // java/lang/Object."<init>":()V#2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;#3 = String             #18            // Hello World#4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V#5 = Class              #21            // com/test/Test#6 = Class              #22            // java/lang/Object#7 = Utf8               <init>#8 = Utf8               ()V#9 = Utf8               Code#10 = Utf8               LineNumberTable#11 = Utf8               main#12 = Utf8               ([Ljava/lang/String;)V#13 = Utf8               SourceFile#14 = Utf8               Test.java#15 = NameAndType        #7:#8          // "<init>":()V#16 = Class              #23            // java/lang/System#17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;#18 = Utf8               Hello World#19 = Class              #26            // java/io/PrintStream#20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V#21 = Utf8               com/test/Test#22 = Utf8               java/lang/Object#23 = Utf8               java/lang/System#24 = Utf8               out#25 = Utf8               Ljava/io/PrintStream;#26 = Utf8               java/io/PrintStream#27 = Utf8               println#28 = Utf8               (Ljava/lang/String;)V
{public com.test.Test();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1                  // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 2: 0public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=1, args_size=10: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc           #3                  // String Hello World5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: returnLineNumberTable:line 4: 0line 5: 8
}
SourceFile: "Test.java"

3.3 访问标志

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

访问标志,如下图

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为public类型

ACC_FINAL

0x0010

是否被声明为final,只有类可设置

ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语意,invokespecial指令的语意在JDK 1.0.2发生过改变,为了区别这条指令使用哪种语意,JDK 1.0.2之后编译出来的类的这个标志都必须为真
ACC_INTERFACE 0x0200 标识这是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其它类型为假
ACC_SYNTHETIC 0x1000

标识这个类并非由用户代码产生

ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举

如下类,对应的访问标志为

public abstract class Demo02{public static void main(String[] args){System.out.println("Demo02");}
}

因其为public修饰的抽象类,所以访问标识对应为:0x0400+0x0020+0x0001=0x0421

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

类索引和父类索引都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,class文件中由这三项数据确定这个类的继承关系。类索引用于确定类的全限定名,父类索引用于确定类的父类的全限定名(Java的单继承特性,父类索引只有一个,除java.lang.Object外,其它类都有父类,父类索引都不为0)。接口索引集合用来描述类实现了那些接口,被实现的接口将按implements或extends后的接口顺序从左到右排列在接口索引集合。

类索引和父类索引引用两个u2类型的索引值表示,各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值找到定义在CONSTANT_Utf-8_info类型的常量中的全权限名字符串。接口索引集合,第一项 u2类型的数据为接口计数器,表示索引表的容量。若该类没有实现任何借口,则计数器值为0,后面接口的索引表不再占用任何字节。

由之前winhex查看class文件可知:类索引为0x0005,父类索引为0x0006,接口索引计数器为0x0000

据字节码内容得到如下信息:

#5 = Class              #21            // com/test/Test
#6 = Class              #22            // java/lang/Object
#21 = Utf8               com/test/Test
#22 = Utf8               java/lang/Object

由此可看到这个class的类全限定名,父类全限定名

3.5 字段表集合

字段表用于描述接口或者类中声明的变量。包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。包括的信息由:字段的作用域(public,private,protected)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。修饰符都是布尔值;字段名,字段数据类型引用常量池中的常量来描述。

字段表结构
类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribate_info attributes attributas_count

由字段表结构可以知道,它使用3个连续的u2数据类型存储

access_flags是字段的访问标识

name_index和descriptor_index,都是对常量池的引用,分别代表字段的简单名称和字段与方法的描述符

字段的简单名称就是类中的字段名和方法名

描述符的作用:用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值

字段访问标志
标志名称 标志值 含义
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修饰

由Java本身的语言规则决定:ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED中只能选一个;ACC_FINAL、ACC_VOLATILE不能同时选择;接口中字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志。

标识字符
标识字符 含义
B 基本类型 byte
C 基本类型 char
D 基本类型 double
F 基本类型 float
I 基本类型 int
J 基本类型 long
S 基本类型 short
Z 基本类型 boolean
V 特殊类型 void
L 对象类型,如Ljava/lang/Object

字段表集合中不会列出从超类或者父类接口中继承而来的字段,但有可能列出原本Java代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。Java语言中不管两个字段的数据类型、修饰符是否相同,都不能使用一样的名称;但对于字节码而言,两个字段的描述符不一致,字段名可以相同。

3.6 方法表集合

方法的描述与字段的描述几乎采用了完全一致的方式,方法表的结构如同字段表一样,依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes);仅在访问标志和属性表集合的可选项中有区别

方法表结构
类型 名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes

attributes_count

因volatile和transient不能修饰方法,方法表的访问标志中没有ACC_VOLATILE、ACC_TRANSIENT;sysnchronized、native、strictfp和abstract可以修饰方法,因此方法表的访问标志中添加了ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志。

方法访问标志
标志名称 标志值 含义
ACC_PUBLIC 0x0001 方法是否被public修饰
ACC_PRIVATE

0x0002

字方法是否被private修饰
ACC_PROTECTED 0x0004 方法是否被protected修饰
ACC_STATIC 0x0008 方法是否被static修饰
ACC_FINAL 0x0010 方法是否被final修饰
ACC_SYNCHRONIZED 0x0020 方法是否被sysnchronized修饰
ACC_BRIDGE 0x0040 方法是否是由编辑器产生的桥接方法
ACC_VARARGS 0x0080 方法是否接受不定参数
ACC_NATIVE 0x0100 方法是否被native修饰
ACC_ABSTRACT 0x0400 方法是否被abstract修饰
ACC_STRICTFP 0x0800 方法是否被strictfp修饰
ACC_SYNTHETIC 0x1000 方法是否是由编译器自动产生的

如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息,可能会出现编译器自动添加的方法,如类构造器"<clinit>",方法和实例构造器"<init>"方法。

Java中重载方法,要与原方法具有相同的简单名称,与原方法不同的特征签名;在对应的class文件中,特征签名就是方法各个参数在常量池中的字段符号引用的集合,但返回值不在其中,因此Java里无法仅仅依靠返回值对一个方法进行重载。但在class文件中,两个方法名称和特征签名相同,返回值不同的方法,可以合法存在与同一个class文件中。

3.7 属性表集合

class文件、字段表、方法表都可以携带自己的属性表集合,用于描述某些场景专有的信息;不要求各个属性表具有严格顺序,只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己的定义的属性信息,JVM运行时会忽略掉它不认识的属性。

属性表结构
类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u1 info attribute_length

3.7.1 Code属性

方法体中代码经Javac编译后,变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合中,但并非所有的方法表都必须存在这个属性,如接口或抽象类中的方法就不存在Code属性。

Code属性表的结构
类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 max_stack 1
u2 max_locals 1
u4 code_length 1
u1 code code_length
u2 exception_table_length 1
exception_info exception_table exception_table_lebth
u2 attributes_count 1
attribute_info attributes attributes_count

attribute_name_index:指向CONSTANT_Utf8_info型常量的索引,常量值固定为"Code",代表了该属性的属性名称

attribute_length:属性值的长度,由于属性名称索引与属性长度共6个字节,所以固定为整个属性表的长度减6个字节

max_stack:代表了操作数栈深度的最大值。JVM运行时需要根据它来分配栈帧中的操作栈深度

max_locals:代表局部变量表所需的存储空间。单位时Slot,Slot是JVM为局部变量分配内存所使用的最小单位。byte、char、float、int、short、boolean和returnAddress等长度不超过32为的数据类型,每个局部变量占用1个Slot;double和long两种64位的数据类型占用2个Slot。方法参数(包括实例方法中隐藏参数"this“)、显示异常处理器的参数(Exception Handler Parameter,就是try-catch语句中catch块所定义的异常)、方法体中定义的局部变量都需要使用局部变量表来存放。

code_length和code用来存储Java源程序编译后生产的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。字节码指令,占用1个字节;字节码指令后面可以跟随参数。code_length是u4类型的长度值,理论最大值为Java中int的最大值,但JVM明确限制其不超过65535,即实际只使用u2的长度。若超过65535了,javac则会拒绝编译。

若将Java程序分为代码(Code,方法体中的代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)两部分,那么Code属性用于描述代码,所有的其他数据项目都用与描述元数据。

在任何实例方法中,都可以通过"this"关键字访问到此方法所属的对象,原因是javac编译器在编译时把this关键字的访问转变为对一个普通方法参数的访问,然后在JVM调用实例方法时自动传入此参数。所以在实例方法的局部变量表中至少存在一个指向当前对象实例的局部变量,局部变量表这也会预留出第一个Slot为来存放对象实例的引用,方法参数值从1开始计算。以上处理只对实例方法有效,若将inc方法声明为static,Args_size则为0。

3.7.2 Exceptions属性

Exceptions属性在方法表中与Code属性平级,列出了方法中可能抛出的手查异常,也就是方法描述是throws关键字后面列举的异常。Exceptions属性中,number_of_exceptions项表示方法可能抛出number_of_exceptions种受查异常;每种受查异常用一个exception_index_table项表示,exception_index_table是一个指向常量池中CONSTANT_Class_info型常量的索引,代表了该受查异常的类型。

属性表结构
类型 名称 数量

u2

attribute_name_index 1
u4 attribute_length 1
u2 number_of_exceptions 1
u2 exception_index_table number_of_exceptions

3.7.3 LineNumberTable属性

LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系,不是运行时必需的属性,默认会生成到class文件中,可在javac中使用-g:none或g:lines选项来取消或要求生成这项信息。若没有生成LineNumberTable属性,在程序运行抛出异常时,堆栈中将不会显示出错的行号;并在调试程序时,无法按照源码行进行设置断点。line_number_table是一个数量为line_number_table_length、类型为line_number_info的集合,line_number_info表包括start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号。

LineNumberTable属性结构
类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 line_number_table_length 1
line_number_info line_number_table line_number_table_length

3.7.4 LocalVariableTable属性

LIneNumberTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系,不是运行时必需的属性,默认生成在class文件中,可在javac使用-g:none或-g:vars 选项来取消或要求生产这项信息。若没有这项属性,影响是当其他人引用这个方法时,所有的参数名称都将会丢失,IDE会使用诸如arg0,arg1之类的占位符替换原有的参数名,对代码编写带来较大不便,在调试期间无法根据参数名称从上下中获得参数值,但对程序运行没有影响。

LocalVariableTable属性结构
类型 名称 数量

u2

attribute_name_index 1
u4 attribute_length 1
u2 local_variable_table_length 1
local_variable_info local_variable_table local_variable_table_length

local_variable_info项目代表了一个栈帧与源码中的局部变量的关联

local_variable_info项目结构
类型 名称

数量

u2 start_pc 1
u2 length 1
u2 name_index 1
u2 descriptor_index 1
u2 index 1

start_pc和length属性分别代表了这个局部变量的生命周期开始的字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码中的作用域范围。name_index和descriptor_index都是指向常量池中CONSTANT_Utf8_info型常量的索引,分别代表了局部变量的名称以及局部变量的描述符。index是这个局部变量在栈帧局部变量表中Slot的位置,若这个变量是double或long类型的,它占用的Slot为index和index+1两个。

在JDk1.5引入泛型之后,增加了LocalVariableTypeTable属性,与LocalVariableTable非常相似,仅把字段描述符descriptor_index替换成了字段特征签名Signfure;对于非泛型来说,二者描述的信息基本一致;但泛型引入之后描述符就不能准确地描述泛型类型。

3.7.5 SourceFile 属性

SourceFile属性用于记录class文件的源码文件名称,可选的,定长的,可以分别使用javac的-g:none和-g:source选项来关闭或要求生成这项信息。在Java中,大多数类的class文件名与源码文件名相同,但有特殊情况(如内部类,class未被public修饰)。若没有生产这项属性,当抛出异常时,堆栈中将不会显示出错代码所属的文件名。

SourceFile属性结构
类型 名称

数量

u2 attribute_name_index 1
u4 attribute_length 1
u2 sourcefile_index 1

sourcefile_index数据项时指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源码文件的文件名。

3.7.6 ConstantValue属性

ConstantValue属性是通知JVM自动为静态变量赋值。只有被static修饰的变量(类变量)才可以使用这项属性。

JVM对”int x = 123“和”static int x = 123"两种变量赋值的方式和时刻是不同的,对于非static类型的变量(实例变量)的赋值是在实例构造器<init>方法中进行的;对于类变量,有两种方式可以选择:在类构造器<clinit>方法中或者使用ConstantValue属性;目前Sun Javac编译器的选择是:若同时使用static和final修饰变量(常量),并且这个变量的数据类型是基本类型或者java.lang.String,就生成ConstantValue属性来进行初始化;若变量没有被fina修饰,或者并非基本类型及字符串,则会选择在<clinit>方法中进行初始化。

ConstantValue属性结构
类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length

1

u2 constantvalue_index 1

ConstantValue属性是一个定长属性,它的attribute_length数据项值必须固定为2,constantvalue_index数据项代表常量池中一个字面量常量的引用,根据字段类型的不同,可以是CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_Integer_info、CONSTANT_String_info常量中的一种。

3.7.7 InnerClasses属性

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

InnerClasses属性结构
类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_classes 1
inner_classes_info inner_classes number_of_classes

数据项number_of_classes代表需要记录多少个内部类信息,每个内部类的信息都由一个inner_classes_info表进行描述。

inner_classes_info表的结构
类型 名称 数量
u2 inner_class_info_index 1
u2 outer_class_info_index 1
u2 inner_name_index 1
u2 inner_class_access_flags 1

inner_class_info_index和outer_info_index都是指向常量池中CONSTANT_Class_info型常量的索引,分别代表了内部类和宿主类的符号引用。

inner_name_index是指向常量池中CONSTANT_Utf8_info型常量的索引,代表类的名称,如果是匿名内部类,这项值为0。

inner_class_access_flags是内部类的访问标志,类似于类的access_flags。

inner_class_access_flags标示
标志名称 标志值 含义
ACC_PUBLIC 0x0001 内部类是否为public
ACC_PRIVATE

0x0002

内部类是否为private
ACC_PROTECTED 0x0004 内部类是否为protected
ACC_STATIC 0x0008 内部类是否为static
ACC_FINAL 0x0010 内部类是否为final
ACC_INTERFACE 0x0020 内部类是否为synchronized
ACC_ABSTRACT 0x0400 内部类是否为abstract
ACC_SYNITHETIC 0x1000 内部类是否并非由用户代码产生的
ACC_ANNOTATION 0x2000 内部类是否是一个注解
ACC_ENUM 0x4000 内部类是否是enum

3.7.8 Deprecated及Synthetic属性

Deprecated和Ssnthetic都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。

Deprecated属性用于表示某个类,字段或方法,已经不再推荐使用,可以通过在代码中使用@deprecated注释进行设置

Syntheti属性代表次字段或方法并不是由Java源码直接产生的,而是由编译器自行添加的。JDK1.5之后,标识一个类、字段、或者方法是编译器自动产生的,也可以设置它的访问标志中的ACC_SYNTHETIC标志位。所有由非用户代码产生的类、方法及字段都应当至少设置Synthetic属性和ACC_SYNTHETIC标志位中的一项,实例构造器"<init>"方法和类构造器"<clinit>"方法除外。

Deprecated及Synthetic属性的结构
类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1

attribute_length数据项的值必须为0x00000000,因为没有任何属性值需要设置。

3.7.9 StackMapTable属性

StackMapTable属性在JDK1.6后增加到class文件规范中,是一个复杂的变长属性,位于Code属性的属性表中。会在虚拟机类加载的字节码验证阶段被新类型检查验证器,代替提前消耗性能的基于数据流分析的类型推导验证器。StackMapTable属性中包含零至多个栈帧,每个栈映射栈帧都显示或隐式地代表一个字节码偏移量,用于表示执行到该字节码时局部变量表和操作数栈的验证类型。类型检查器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束。

StackMapTable属性的结构
类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_entries 1
stack_map_frame stack_map_frame_entries number_of_entries

《Java虚拟机规范(Java SE 7版)》明确规定:在版本号大于或等于50.0的class文件中,如果方法的Code属性没有附带StackMapTable属性,它将带有一个隐式的StackMapTable属性。它等同于number_of_erntries值为0的StackMapTable属性。一个方法的Code属性最多只能有一个StackMapTable属性,否则将抛出ClassFormatError异常。

3.7.10 Signature属性

JDK1.5后增加的,可选的定长属性,可以出现在类、属性表和方法表结构的属性表中,与泛型有关。

Signature属性的结构
类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u2 signature_length 1

signature_length项的值必须是一个对常量池的有效索引,且必须是CONSTANT_Utf8_info结构,表示类的签名,方法类型签名或字段类型签名。

3.7.11 BootstrapMethods属性

JDK1.7后增加的,复杂的变长属性,位于类文件的属性表中,用于保存invokedynamic指令的引导方法限定符。与JSR-292中的InvokeDynamic指令、java.lang.Invoke包关系密切。

4. 字节码指令简介

Java虚拟机指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码)以及跟随其后的零至多个代表此操作所需参数(操作数)构成。Java虚拟机采用面向操作数栈而不是寄存器的架构,大多数的指令都补包含操作数,只有一个操作码。由于限制了Java虚拟机操作码的长度为1个字节,意味着指令集的操作码总数不可能超过256条。

4.1 字节码与数据类型

Java虚拟机的指令集中,大多数的指令都包含了其操作所对应的数据类型信息。i代表对int类型的数据类型操作、l代表long、s代表short、b代表byte、c代表char、f代表float、d代表double、a代表reference

4.2 加载和存储指令

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

将一个局部变量加载到操作栈:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>。

将一个数值从操作数栈到局部变量表:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>。

将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>。

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

以尖括号结尾的,如iload_<n>,实际代表了一组指令(iload_0、iload_1、iload_2和iload_3),都是某个带有操作数的通用指令(例如iload)d的特殊形式,省略掉显式的操作数,不需要进行取操作数的动作,实际操作数隐藏在指令中,例如iload_0的语义与操作数为0时的iload指令完全一致。

4.3 运算指令

运算符或算术指令用于两个操作数栈上的值进行某种特定的运算,并把结果重新存入到操作栈帧。算术指令分为两种:对整数型数据进行运算的指令与对浮点数型数据进行运算的指令,它们都使用Java虚拟机的数据类型,由于,欸有直接支持buye、short、char和boolean类型的算术指令,对这类数据类型的运算,应使用操作int类型的指令代替。整数与浮点数在溢出和被零除的时候有各自不同的行为表现。

加法指令:iadd,ladd,fadd,dadd。

减法指令:isub,lsub,fsub,dsub。

乘法指令:imul,lmul,fmul,dmul。

除法指令:idiv,ldiv,fdiv,ddiv。

求余指令:irem,lrem,frem,drem。

取反指令:ineg,lneg,fneg,dneg。

位移指令:ish1,ishr,iushr,lshl,lshr,lushr。

按位或指令:ior,lor。

按位与指令:iand,land。

按位异或指令:ixor,lxor。

局部变量自增指令:iinc。

比较指令:dcmpg,dcmpl,fcmpg,fcmpl,lcmp。

Java虚拟机规定在处理整型数据时,只有除法(idiv和ldiv)以及求余指令(irem和lrem)中当出现除数为零时会导致虚拟机抛出ArithmeticException异常,其余任何整型数运算场景都不应该抛出运行时异常。

Java虚拟机必须完全支持IEEE 754中定义的非正规浮点数值和逐级下溢的运算规则。Java虚拟机在处理浮点数运算时,不会抛出任何运行时异常(Java语言中的异常),当一个人操作产生溢出时,将会使用有符号的无穷大来表示;如果走个操作结果没有明确的数学定义,将会使用NaN值表示。所有使用NaN值作为操作数的算术操作,结果都会返回NaN。

4.4 类型转换指令

类型转换指令可以将两种不同的数值类型进行相互转换,一般用于实现用户代码中的显示类型转换操作,或者用来处理字节码指令集中类型相关指令无法与数据类型一一对应的问题。

Java虚拟机直接支持(即转换时无需显式的转换指令)一下数值类型的宽化类型转换(即小范围类型子昂大范围类型的安全转换):

int 类型到long、float、或者double类型。

long类型到float、double类型。

float类型到double。

处理窄化类型转换时,必须显式地使用转换指令完成,包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。窄化类型转换可能会导致转换结果产生不同地正负号,不同的数值级的情况,转换过程很可能会导致数值的精度丢失。

将int或long类型窄化转换为整数类型T的时候,转换过程仅仅是简单地丢弃除最低位N个字节以外地内容,N值类型T地数据类型长度。并且得到是转换后结果地补码形式。

浮点值窄化转换为整数类型T(T限于int或long类型之一)时,将遵循:如果浮点值时NaN,转换结果时int或long类型的0。如果浮点值不是无穷大,浮点值使用IEEE 754的项零舍入模式取整,或者整数值v,如果v在目标类型T(int或long)的表示范围之内,转换结果就是v;否则,将根据v的符号,转换为T所能表示的最大或者最小数。如果浮点值时Infinity,则转换结果为对应T类型的最大值或最小值。

Java虚拟机规范中明确规定数值类型的窄化转换指令永远不可能导致虚拟机抛出运行时异常。

4.5 对象创建与访问指令

Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。,对象创建后,可以通过对象访问指令获取对象实例或许数组实例中三位字段或者数组元素。

创建类实例的指令:new。

常见数组的指令:newarray、anewarray、multianewarray。

访问类字段(static字段,类变量)和实例字段(非static字段,实例变量)的指令:getstatic、putstatic;getfield、putfield。

把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload。

将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore,dastore,aastore。

取数组长度的指令:arraylength。

检查类实例类型的指令:instanceof、checkcast。

4.6 操作数栈管理指令

Java虚拟机提供了一些用于直接操作操作数栈的指令。

将操作数栈的栈顶一个或两个元素出栈:pop、pop2。

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

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

4.7 控制转移指令

可以让Java虚拟机有条件或无条件地指定的位置指令而不是控制转移指令的下一条指令继续执行程序。可以认为控制转移指令就是在有条件或无条件地修改PC寄存器地值。

条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。

复合条件分支:tableswitch、lookupswitch。

无条件分支:goto、goto_w、jsr、jsr_w、ret。

对于boolean类型、byte类型、char类型和shprt类型地条件分支比较操作,都是使用int类型地比较指令完成的;对于long类型、float类型和double类型的条件分支比较操作,则会先执行相应类型的比较运算指令(dcmpg、dcmpl、fcmpg、fcmpl、lcmp),运算指令会返回一个整型值到操作数栈中,再执行int类型的条件分支比较操作完成整个分支跳转。

4.8 方法调用和返回指令

invokevirtual指令用于调用对象的实例方法,根据对象的实例类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。

invokeinterface指令用于调用接口方法,它会再运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。

invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法,私有方法和分类方法。

invokestatic指令用于调用类方法(static方法)。

invokedynamic指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。

方法调用指令与数据类型无关,二方法返回指令时根据返回值的类型区分的,包括ireture(当返回值时boolean,byte,char,short和int类型时使用)、lreturn、freture、dreturn和areturn,return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。

4.9 异常处理指令

Java程序中显式抛出异常的操作(throw语句)都是由athrow指令来实现的;除用throw显式抛出异常外,Java虚拟机规定许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。

在Java虚拟机中,处理异常不是由字节码指令来实现的,而是采用异常表来完成的。

4.10 同步指令

Java虚拟机支持方法级的同步和方法内部一段指令序列的同步,都是使用管程来支持的。Java中同步一段代码指令序列通常是由synchronized语句块来表示,Java虚拟机的指令集中有monitorenter和monitorexit两条指令支持synchronized关键字的语义。

《深入理解Java虚拟机》第六章 类文件结构 — 读书笔记相关推荐

  1. 深入理解Java虚拟机-第六章 类文件结构

    第六章 类文件结构 6.1 概述 略 6.2 无关性的基石 因为想要实现 "Write Once,Run Anywhere"的伟大理想,Java 虚拟机被发明了出来.这些虚拟机都可 ...

  2. [深入理解Java虚拟机]第六章 Class类文件的结构

    在本章关于Class文件结构的讲解中,我们将以<Java虚拟机规范(第2版 )> (1999年发布,对应于JDK 1.4时代的Java虚拟机)中的定义为主线,这部分内容虽然古老,但它所包含 ...

  3. 深入理解java虚拟机章节_深入理解java虚拟机-第六章

    第6章 类文件 6.3 Class类文件的结构 Class文件是一组以8位字节为基础单位的二进制流. Class文件格式采用一种类似C语言结构伪结构存储数据,这种伪结构中只有两种数据类型:无符号数和表 ...

  4. 深入理解Java虚拟机(周志明)——读书笔记1

    第二章:Java内存区域与内存溢出异常 一.运行时数据区域 首先上图,想必大家都很想吐槽,这张图我都要看烂了. 程序计数器 线程私有. 它是程序控制流的指示器,分支.循环.跳转.异常处理.线程恢复等基 ...

  5. 《深入理解Java虚拟机》内存管理机制 部分 读书笔记

    内存管理 运行时的数据区包括: 程序计数器 一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器. 每个下次都需要有一个独立的程序计数器,各线程间计数器互不影响,独立存储. 如果线程执行的 ...

  6. 深入理解java虚拟机gc_jvm GC收集器与内存分配(深入理解java虚拟机第三章)

    jvm GC收集器与内存分配(深入理解java虚拟机第三章) 本篇是<深入理解java虚拟机第三章>的笔记记录. 一 为什么要关注GC和内存分配? 需要排查各种内存溢出.内存泄漏问题时,或 ...

  7. 深入理解Java虚拟机(第3版)学习笔记——JAVA内存区域(超详细)

    深入理解Java虚拟机(第3版)学习笔记--JAVA内存区域(超详细) 运行时数据区域 程序计数器 java虚拟机栈 本地方法栈 java堆 方法区 运行时常量池 直接内存 对象的创建 对象的内存布局 ...

  8. 【读书笔记】实战JAVA虚拟机JVM故障诊断与性能优化 读书笔记

    文章目录 1.概述 1.1 **第一章:初探java虚拟机** 1.2 认识java虚拟机的基本结构 1.3 常用Java虚拟机参数 1.4 垃圾回收器 1.5 垃圾收集器以及内存分配 1.6 性能监 ...

  9. 读-深入理解Java虚拟机(1-2章)随笔

    笔者花了一个星期的晚上时间看完了前面三章,由于之前在其他博客看过JVM的东西,所以看起来比较快. 前l两章内容分别是--走近Java:Java内存区域与内存溢出异常.其中部分除黑色内容属于扩展性内容或 ...

最新文章

  1. Linux系统下的/etc/nsswitch.conf文件
  2. Android提升篇系列:Android项目代码优化实践
  3. Java 的类加载顺序
  4. linux用数字方式显示文件权限,全面解析Linux数字文件权限
  5. 12.1简介Object类
  6. 深入理解分布式技术 - 服务注册与发现背后的逻辑
  7. 怎么判断前轮左右的位置_汽车上主要传感器的安装位置,你知道多少?
  8. Microsoft SQL Server 全角转半角函数
  9. 美国插画家Mike Bear作品欣赏
  10. C#笔记04 数组和循环
  11. LA 2572 Viva Confetti (Geometry.Circle)
  12. python单词的含义-python
  13. print python 带回车_python标准库threading源码解读【二】
  14. lableme标注的json文件转为mask r-cnn训练用的coco数据集格式
  15. android形状属性、锁屏密码、动态模糊、kotlin项目、抖音动画、记账app、视频播放器等源码...
  16. 企业信息化与BI系统建设规划
  17. 山西台达plc可编程控制器_汇川PLC可编程控制器的功能特点
  18. win10一共几个版本,有什么区别?win10版本区别
  19. xp计算机用户名和密码忘记了怎么办,忘记XP的用户名和密码怎么办?
  20. 崩坏3卡池模拟器及毕业期望概率计算(含保底)

热门文章

  1. IT的成功人士学习之道
  2. 分布式与并行计算课程设计(附流程分析图)
  3. 解决idea中插件商店加载不出来插件问题
  4. 公司为何要搭建一套呼叫中心系统
  5. PAT(pat)乙级合级(全)C语言
  6. Python 之父 Guido van Rossum 称退休太无聊,正式加入微软搞开源!
  7. php asp.net core,ASP.NET Core集成微信登录的实例图解
  8. 解决ARP攻击的方法和原理(转)
  9. 知行合一,持续输出,以不变应万变
  10. 技术女性职业规划及其它