"In-depth understanding of the Java virtual machine" reading notes (5)-class file structure

Note: The fifth chapter in the book-tuning case analysis and actual combat, contains several case analysis and eclipse tuning, but the examples are very simple, so I won’t put it in the notes.

table of Contents

 

1. Irrelevance

Two, Class file structure

2.1 Magic number and class file version

2.2 Constant pool

javap

2.3 Access mark

2.4 Collection of class index, parent class index and interface index

2.5 Field Table Collection

2.6 Method table collection

2.7 Attribute Table Collection

2.7.1 Code

2.7.2 Exceptions

2.7.3 LineNumberTable

2.7.4 LocalVariable Table

2.7.5 InnerClass attribute

2.7.6 Deprecated及Synthetic

2.7.7 StackMapTable

2.7.8 Signature

2.7.9 BootstrapMethods

Three, bytecode instructions

3.1 Bytecode and data type

3.2 Classification of bytecode usage

Four, summary


1. Irrelevance

The basis for language independence is the virtual machine and bytecode storage format. The Java virtual machine is not bound to any language including Java. It is only associated with the specific binary file format of the class file. The Java virtual machine does not Care about the language of the source of the class. For example, languages ​​such as Groovy and Scala can produce class files that conform to the specification: the Java virtual machine specification requires the use of many mandatory syntax and structural constraints in class files.

Two, Class file structure

The class file is a set of binary streams with 8-bit (1 byte) as the basic unit . Each data item is arranged in the class file in strict order and compactly, without any separator in the middle. When a data item that needs to occupy more than 1 byte of space is encountered, it will be divided into a number of 1 byte for storage in a high-order manner.

It contains two data types: unsigned numbers and tables.

  • Unsigned number: the basic data type. U1, u2, u4, u8 are used to represent 1 byte, 2 bytes, 4 bytes and 8 bytes of unsigned numbers respectively. Unsigned numbers can be used Describe numbers, index references, quantity values ​​or compose string values ​​according to UTF-8 encoding.

  • Table: A composite data type composed of multiple unsigned numbers or other tables as data items. All tables habitually end with "_info". The table is used to describe the data of the composite structure with hierarchical relationships, and the entire class file is essentially a table.

The class file is mainly composed of the following data items (in order)

class file format
Types of name meaning Quantity
u4 magic Magic number 1
u2 minor_version Minor version 1
u2 major_version Major version 1
u2 constant_pool_count Constant pool size 1
cp_info constant_pool Constant pool constant_pool_count-1
u2 access_flags Access mark 1
u2 this_class Current class 1
u2 super_class father 1
u2 interfaces_count Number of interfaces 1
u2 interfaces interface interfaces_count
u2 fields_count Field table size 1
field_info fields Field table fields_count
u2 methods_count Method table size 1
method_info methods Method table methods_count
u2 attributes_count Attribute table size 1
attribute_info attributes Attribute table attributes_count

Note: The attribute table is arranged last in the table, but in fact, the class, field table, and method table may contain corresponding attribute tables, and the attribute table is not a separate part.

2.1 Magic number and class file version

The first 4 bytes of each class file is called the magic number (Magic Number) , its only function is to determine whether the file is a class file that can be accepted by the virtual machine, and it is fixed at 0xCAFEBABE .

The 4 bytes following the magic number store the version number of the class file, the 5th and 6th bytes are the minor version number, and the 7th and 8th bytes are the major version number. The Java version number starts from 45. The higher version of the JDK can be backward compatible with the lower version of the class file, but the higher version of the class file cannot be run. Even if the file format does not change in any way, the virtual machine must refuse to execute beyond its version No. of class files.

2.2 Constant pool

Following the minor version number is the entry of the constant pool, which can be understood as the resource warehouse in the class file. The size of the constant pool of each class may be different, so you need to place a u2 type of data at the entry of the constant pool, which represents the constant pool capacity counter. Note that this capacity counter starts from 1, not 0. In other words, if the capacity of the constant pool is 0x0016, which is 22 in decimal, it means that there are 21 constants in the constant pool, and the index value ranges from 1 to 21. The purpose of vacating the 0th item is to satisfy that some data that points to the index value of the constant pool need to express the meaning of "not referencing any constant pool item" under certain circumstances. In the class file structure, only the capacity counter of the constant pool starts from 1, and everything else starts from 0.

There are mainly two types of constants stored in the constant pool:

  • Literal : It is closer to the concept of constants at the Java language level, such as text strings, final modified constant values, etc.
  • Symbol reference : the concept belongs to the principle aspects of compiling, comprising a fully qualified name of the class and interface , the name and descriptor fields , the method name and descriptor .

When the Java code is compiled, there is no "connection" step like c and c++, but it is dynamically connected when the JVM loads the class file. The final memory layout information of methods and fields will not be saved in the class file. When the JVM is running, the corresponding symbol reference needs to be obtained from the constant pool, and then parsed and translated into specific memory during class creation or runtime.

Each constant in the constant pool is a table . Before JDK1.7, there were 11 table structures with different structures, and three new table structures were added in JDK1.7. These 14 types of tables have one thing in common: the first bit at the beginning of the table is a flag bit of type u1 , which represents which constant type the constant belongs to. The specific meanings of these 14 constant types are shown in the following table:

Item type of constant pool
constant Project (structure) Types of description
CONSTANT_Utf8_info tag u1 Value 1
length u2 The number of bytes occupied by a UTF-8 encoded string
bytes u1 UTF-8 encoded string with length length
CONSTANT_Integer_info tag u1 Value 3
bytes u4 According to the int value stored before the high order
CONSTANT_Float_info u1 Value 4
tag
bytes u4 According to the float value stored first
CONSTANT_Long_info tag u1 Value 5
bytes u8 According to the long value stored first
CONSTANT_Double_info tag u1 Value 6
bytes u8 Double value stored in front of the highest order
CONSTANT_Class_info tag u1 Value 7
index u2 Index to the fully qualified name constant item
CONSTANT_String_info tag u1 Value 8
index u2 Index to string literal
CONSTANT_Fieldref_info tag u1 Value 9
index u2 Point to the index item of the class or interface descriptor CONSTANT_Class_info of the declared field
index u2 Point to the index entry of the field descriptor CONSTANT_NameAndType
CONSTANT_Methodref_info tag u1 Value 10
index u2 Point to the index entry of the class descriptor CONSTANT_Class_info of the declared method
index u2 Index entry pointing to the name and type descriptor CONSTANT_NameAndType
CONSTANT_InterfaceMethodref_info tag u1 Value is 11
index u2 Point to the index entry of the interface descriptor CONSTANT_Class_info of the declared method
index u2 Index entry pointing to the name and type descriptor CONSTANT_NameAndType
CONSTANT_NameAndType_info tag u1 Value 12
index u2 Index to the constant item of the field or method name
index u2 Index to the constant item of the field or method descriptor
CONSTANT_MethodHandle_info tag u1 Value 15
reference_kind u2 范围[1,9],它决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为
reference_index u2 值必须是对常量池的有效索引
CONSTANT_MethodType_info tag u1 值为16
descriptor_index u2 值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示方法描述符
CONSTANT_InvokeDynamic_info tag u1 值为18
bootstrap_method_attr_index u2 值必须是对当前class文件中引导方发表的bootstrap_methods[]数组的有效索引
name_and_type_index u2 值必须是对当前常量池的有效索引,常量池在该索引处的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符

有了上表的项目类型定义,我们就可以分析字节码了,以下面的字节码文件为例:

我们跳过前8个字节的魔数+次主版本号,获取2个字节的常量池大小:0x002c,即十进制的44,这代表常量池中有43项常量。然后看第一项常量的tag,值为0x0a,即十进制的10,在上表中找到tag为10的常量池项目,发现为:CONSTANT_Methodref_info,其包含两个u2类型的index:0x0006和0x001e,即十进制的6和30,代表的也是常量池中的位置,这里中间隔了几个常量项,就不继续分析了。

javap

JDK专门提供了用于分析class文件的工具:javap,使用javap分析该字节码(只列出了常量池部分),可以看到,和我们分析的第一项常量是一样的:

2.3 访问标记

常量池结束后,紧接着的两个字节代表访问标记(access_flags),这个标记用于标识一些类或者接口层次的访问信息,包括:

访问标记
标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为public
ACC_FINAL 0x0010 是否被声明为final
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语意。该指令的语意在JDK1.2发生过改变,为了区别这条指令使用哪种语意,JDK1.0.2之后编译的字节码这个标志都必须为真
ACC_INTERFACE 0x0200 标识是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口或抽象类来说,此标志为真,其它都为假
ACC_SYSTHETIC 0x1000 标识这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 标识是一个注解
ACC_ENUM 0x4000 标识是一个枚举

 

access_flags占两个字节,一共16位,所以有16个标志位可以使用,当前只定义了其中8个,没有使用到的标志位要求一律为0。如果多个标志同时存在,那么使用按位或(|)运算组合,判断标志位时,使用按位与(&),判断结果是否大于0即可。

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

类索引和父类索引都是一个u2类型的数据,而接口索引是一组u2类型的数据的集合,class文件中由这三项数据来确定类的继承关系。

  • 类索引:用于确定这个类的全限定名,指向一个类型为CONSTANT_Class_info的类描述符常量,通过该常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。
  • 父类索引:用于确定这个类的父类的全限定名,由于Java不允许多继承,所以父类索引只有一个,指向一个类型为CONSTANT_Class_info的类描述符常量,同类索引。
  • 接口索引集合:描述这个类实现了哪些接口,这些被实现的接口将按照implements语句(如果类本身就是一个接口,那应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。由于接口可能存在多个,所以接口索引集合的入口是一个u2类型的接口计数器,表示接口数量,如果该类没有实现任何接口,那么该计数器为0,如果计数器为0,那么后面接口的索引表不占用任何字节。

2.5 字段表集合

字段表用于描述接口或者类中声明的变量。字段包括类级变量和实例级变量,但不包括在方法内部声明的局部变量。一个字段可以包含的信息有:

  • 字段的作用域:public private等

  • 实例变量还是类变量:static修饰符

  • 可变性:final修饰符

  • 并发可见性:volatile修饰符

  • 可否被序列化:transient修饰符

  • 字段数据类型:基本类型、对象、数组

  • 字段名称

上述信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合用标志位来表示,参考前面的访问标记。而字段名称、字段数据类型都是无法固定的,只能引用常量池中的常量来描述。字段表结构如下:

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

 

其中的access_flags和前文中的类访问标记类似,这里就不提了。跟着access_flags的name_index和descriptor_index分别代表字段的简单名称以及字段和方法的描述符。

注:

简单名称:指没有类型和参数修饰的方法或字段名称,比如void say()方法和int i字段的简单名称分别为"say"和"i"。
描述符:描述符用来描述字段的数据类型、方法的参数列表和返回值。根据描述符规则,基本数据类型以及void都使用对应名称的首字母大写表示,而对象类型使用字符”L“加对象的全限定名表示,数组的每一维度都是用一个前置的”[“字符来描述。如一个定义为"java.lang.String[][]"类型的二维数组,将被记录为:"[[Ljava/lang/String","int[]"被记录为:"[I"。

全限定名:类似com/loren/test/MainClass,就是把类全名中的“.”替换成了”/“,为了使连续多个全限定名之间不产生混淆,使用时最后会加入一个”;“表示全限定名结束。

字段表包含的固定数据项目到descriptor_index就结束了,不过在descriptor_index之后跟随着一个属性表集合用于存储一些额外的信息。比如对于声明:”final static int m = 123;“那就可能会存在一项名为ConstantValue的属性,其值指向常量123。

2.6 方法表集合

方法表的结构和前面字段表一样,依次包含了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。

相对于字段来说,方法表的访问标志中没有了ACC_VOLATILE标志和ACC_TRANSIENT标志。与之相对的,新增了ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志。

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

与字段表集合相对应的,如果父类方法在子类中没有被重写,那么方法表集合中就不会出现来自父类的方法信息,同样的,有可能会出现编译器自动添加的方法,最典型的就是类构造器"<clinit>"和实例构造器"<init>"方法。

注:在Java语言层面,方法的重载除了要与原方法有相同的简单名称外,还要求必须拥有一个与原方法不同的特征签名:方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不包含在特征签名中,因此Java语言无法仅仅依靠返回值不同来对一个已有方法进行重载。但是在class字节码层面,只要描述符不完全一致,两个方法就能共存,也就是只要返回值不同的两个方法都能共存于一个class文件中。

2.7 属性表集合

前文提到过几次,属性表不是单独的一部分,而是由class文件、字段表、方法表等携带,以描述某些场景专有的信息。属性表集合没有那么严格的限制,只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。在<<Java虚拟机规范(Java SE 7)>>版中,预定义了21项属性,这里简单举几个例子(属性表中的属性太多,处于篇幅考虑,这里就以Code属性详细说明,其他属性就进行简单总结):

2.7.1 Code

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_length
u2 attributes_count 1
attribute_info attributes attributes_count

其中,attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,固定值为“Code”,代表了该属性的名称。attribute_length指示了属性值的长度,由于属性名称索引和属性长度一共占6个字节,所以属性值的长度固定为这个属性的属性表长度减去6个字节。由于属性的结构可以完全自定义,所以通过attribute_length说明属性值占的长度即可,根据长度将属性值读取出来,再根据attribute_name_index确定到底该如何解析。也就是如果要跳过一个属性,那么就是跳过2+4+attribute_length个字节。

max_stack表示操作数栈的最大深度,在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行时需要根据这个值来分配栈帧中的操作数栈深度。

max_locals代表了局部变量表所需要的存储空间,单位是Slot,Slot是虚拟机为局部变量分配内存所使用的的最小单位,对于不超过32位的数据类型,占用1个Slot,对于long和double这两种64位的数据类型需要2个Slot。另外,局部变量表中的Slot可以重用,当代码执行超出一个局部变量的作用域时,这个变量所占的Slot可以被其他局部变量所用。

code_lengthcode用来存储方法编译后生成的字节码指令。code_length代表字节码长度,code用于存储代表字节码指令的一系列u1类型的字节流。当虚拟机读取到code中的一个字节码时,就可以对应找出这个字节码代表的是什么指令,并且可以知道该指令后面是否需要跟随参数,以及参数应当如何理解。u1类型可以表达256条指令。关于code_length,虽然是u4类型,但是实际只是用了2个字节,也就是限制了一个方法不允许超过65535条字节码指令,如果超过这个限制,虚拟机会拒绝编译。

注:对于实例方法,编译器在编译的时候,会把this关键字的访问转变为一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数。因此在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个Slot位来存放对象实例的引用,也就是通过javap的结果可以看到,实例方法的Args_size>=1,Locals>=1。静态方法则不会出现这个情况。

在字节码指令之后的是这个方法的显示异常处理表,异常表包含4个字段,描述的含义是:如果字节码在start_pc行到第end_pc行(不包含)之间出现了类型为catch_type或其子类的异常,则转到第handler_pc继续处理,可参考通过字节码理解try-catch-finally

2.7.2 Exceptions

不同于Code中的异常表,这里的Exceptions属性和Code属性平级,作用是列举出方法中可能抛出的受查异常,也就是方法描述时再throws关键字后面列举的异常。

2.7.3 LineNumberTable

用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。它不是运行时必须的属性,但默认会生成到class文件之中。可以在javac中分别使用-g:none或-g:lines选项来取消或要求生成这项信息。如果没有该属性,对程序产生的主要影响就是当抛出异常时,堆栈中就不会显示出错的行号,并且在调试程序的时候也无法按照源码行来设置断点。

2.7.4 LocalVariable Table

用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。它不是运行时必须的属性,但默认会生成到class文件之中。可以在javac中分别使用-g:none或-g:vars选项来取消或要求生成这项信息。如果没有该属性,也对程序运行没有什么影响,但是当其他人引用这个方法时,所有的参数名称都将丢失,IDE将会使用诸如arg0、arg1之类的占位符来代替原有的参数名。

2.7.5 InnerClass属性

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

2.7.6 Deprecated及Synthetic

两个都属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。Deprecated属性用于标识某个类、字段或方法,已经被程序推荐不再使用。Synthetic代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的。

2.7.7 StackMapTable

包含0至多个栈映射帧,每个栈映射帧都显式或隐式地代表了一个字节码偏移量,用于表示该执行到该字节码时,局部变量表和操作数栈的验证类型。类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束。

2.7.8 Signature

由于JAVA语言的泛型采用的擦除法实现的伪泛型。由于泛型擦除,原有的泛型类型会被替换为泛型上限(如果没有指定上限,则为Object),所以在运行期无法通过反射获得真实的泛型信息。Signature属性就是为了弥补这个缺陷而增设的,现在Java的反射API能够获取泛型类型,最终的数据来源也就是这个属性。它可以出现于类、字段表、方法表结构的属性表中。

2.7.9 BootstrapMethods

位于类文件的属性表中,用于保存invokedynamic指令引用的引导方法限定符。

三、字节码指令

Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的0至多个所需参数(称为操作数,Operands)构成。由于Java虚拟机采用面向操作数栈的架构,而不是寄存器,所以多大数的指令都不包含操作数,只有一个操作码(追求小数量、高传输效率),对操作数栈进行出栈、入栈操作。由于操作码的长度为一个字节,所以指令集的操作码总数不能超过256条。

3.1 字节码与数据类型

大多数的指令都包含了其操作所对应的数据类型信息。比如,iload指定用于从局部变量表加载int型数据到操作数栈,而fload指令加载的则是float类型的数据。但是前面提到操作码只有一个字节,如果每一种与数据类型相关的指令都支持Java虚拟机所有运行时数据类型的话,那指令的数量绝对会超出一个字节所能表示的数字范围。

因此Java虚拟机的指令集对于特定的操作只提供了有限的类型相关指令去支持。例如load指令有操作int类型的iload,但是没有操作byte类型的同类指令。大部分的指令都没有支持整数类型byte、char和short,甚至没有任何指令支持boolean类型。编译器会在编译期或运行期将byte和short类型的数据带符号扩展为相应的int类型数据,将boolean和char类型的数据零位扩展为相应的int类型数据。与之类似,在处理boolean、byte、short和char类型的数组时,也会转换为使用对应的int类型的字节码指令处理。因此大多数对于boolean、byte、short、char类型数据的操作,实际上都是使用相应的int类型作为运算类型。

3.2 字节码用途分类

  • 加载和存储指令:用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。比如iload、istore、bipush等。

  • 运算指令:用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作数栈顶。比如加法指令:iadd,减法指令:isub等等。

  • 类型转换指令:将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显示类型转换操作,或者处理前面提到的指令集中数据类型相关指令无法与数据类型一一对应的问题(byte、short等扩展为int)。

  • 对象创建与访问指令:要注意Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。创建类实例:new,创建数组:nwarray、anewarray等。

  • 操作数栈管理指令:类似于操作普通数据结构中的栈,Java虚拟机提供了一些用于直接操作操作数栈的指令。比如pop、dup、swap等。

  • 控制转移指令:可以让Java虚拟机有条件或无条件的修改程序计数器的值。包括条件分支(比如ifeq)、复合条件分支(比如tableswitch)、无条件分支(比如goto)等等。

  • 方法调用和返回指令:方法调用指令包括,像invokevirtual指令:用于调用对象的实例方法,invokespecial指令:调用一些需要特殊处理的方法,包括实例初始化方法、私有方法和父类方法;方法调用指令与数据类型无关,但方法返回指令是根据返回值类型区分的,包括ireturn(返回boolean、byte、char、short、int),lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法以及类和接口类初始化方法使用。

  • 异常处理指令:Java程序中显示抛出异常的操作(throw)都是用athrow指令来实现。除此之外,Java虚拟机规范还规定了许多运行时异常会在其他Java虚拟机指令检测到异常状况时自动抛出。比如在整数运算中,当除数为0时,虚拟机会在idiv或ldiv指令中抛出ArithmeticException异常。现在在Java虚拟机中处理异常是采用异常表完成的,以前则使用的是jsr和ret指令实现。

  • 同步指令:synchronized语句块对应的指令就是monitorenter和monitorexit。编译器必须确保无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都必须执行其对应的monitorexit指令。所以为了保证在方法异常完成时,monitorenter和monitorexit指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可以处理所有的异常。关于synchronized的深入理解,可参考Java并发编程(九):深入JVM 核心全面理解 Synchronized

四、总结

理解字节码文件结构和分析字节码指令是每一个Java程序员都应该掌握的基础技能。本章主要介绍了class文件的结构组成、class数据的存储和访问、字节码指令的一些描述。ava项目安全发布--Jar包(class)加解密实践中应用式的操作了字节码,感兴趣的可以瞅瞅~~

Guess you like

Origin blog.csdn.net/huangzhilin2015/article/details/113926823