深入探索编译插桩技术(三、解密 JVM 字节码)

u2 interfaces[interfaces_count]; // 实现接口信息
u2 fields_count; // 字段数量
field_info fields[fields_count]; // 包含的字段信息
u2 methods_count; // 方法数量
method_info methods[methods_count]; // 包含的方法信息
u2 attributes_count; // 属性数量
attribute_info attributes[attributes_count]; // 各种属性
}

对于 Class 表结构而言,其 前 8 个字节 依次是如下 三个元素

  • 1)、magic每个 Class 文件的头 4 个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机所接受的 Class 文件。很多文件存储标准中都使用魔数来进行身份识别, 譬如图片格式,如 gif 或者 jpeg 等在文件头中都存有魔数。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。并且,Class 文件的魔数获得很有 “浪漫气息”,值为:0xCAFEBABE(咖啡宝贝)
  • 2)、minor_version2 个字节长,表示当前 Class 文件的次版号
  • 3)、major_version2 个字节长,表示当前 Class 文件的主版本号。(Java 的版本号是从 45 开始 的,JDK 1.1 之后的每个 JDK 大版本发布会在主版本号向上加 1(JDK 1.0~1.1 使用了 45.0~45.3 的版本号),例如 JDK 1.8 就是 52.0)。需要注意的是,虚拟机会拒绝执行超过其版本号的 Class 文件

然后,我们再来简单地了解下 其它元素 的含义:

  • 4)、constant_pool_count常量池数组元素个数
  • 5)、constant_pool常量池,是一个存储了 cp_info 信息的数组,每一个 Class 文件都有一个与之对应的常量池。(注意:cp_info 数组的索引从 1 开始)
  • 6)、access_flags表示当前类的访问权限,例如:public、private
  • 7)、this_class 和 super_class存储了指向常量池数组元素的索引,this_class 中索引指向的内容为当前类名,而 super_class 中索引则指向其父类类名
  • 8)、interfaces_count 和 interfaces同上,它们存储的也只是指向常量池数组元素的索引。其内容分别表示当前类实现了多少个接口和对应的接口类类名
  • 9)、fields_count 和 fields表示成员变量的数量和其信息,信息由 field_info 结构体表示
  • 10)、methods_count 和 methods表示成员函数的数量和它们的信息,信息由 method_info 结构体表示
  • 11)、attributes_count 和 attributes表示当前类的属性信息,每一个属性都有一个与之对应的 attribute_info 结构。常见的属性信息如调试信息,它需要记录某句代码对应源代码的哪一行,此外,如函数对应的 JVM 字节码、注解信息也是属性信息

需要注意的是,Class 表的结构不像 XML 等描述语言,由于它没有任何分隔符号,所以在上面中的这些数据项,无论是顺序还是数量,甚至于数据存储的字节序(Byte Ordering,Class 文件中字节序为 Big-Endian)这样的细节,都是被严格限定的

对于上面的各个属性来说,有不少属性是我们需要重点掌握的,而 常量池可以被认为是 Class 表结构中的重中之重。下面👇,我们就先来了解下常量池。

二、常量池

常量池可以理解为 Class 文件之中的资源仓库,其它的几种结构或多或少都会最终指向到这个资源仓库之中

此外,常量池是 Class 文件结构中与其他项 关联最多 的数据类型,也是 占用 Class 文件空间最大 的数据项之一,同时它还是 在 Class 文件中第一个出现的表类型数据项。因此,如果没有充分了地解常量池,后面其它的 Class 表类型数据项的学习会变得举步维艰。

假设一个常量池的容量(偏移地址:0x00000008)为十六进制数 0x0016,即十进制的 22,这就代表常量池中有 21 项常量,索引值范围为 1~21。在 Class 文件格式规范制定之时,设计者将第 0 项常量空出来是有特殊考虑的,这样做的目的在 于满足后面某些指向常量池的索引值的数据在特定情况下需要表达 “不引用任何一个常量池项”的含义

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

1、字面量(Literal)

字面量比较接近于 Java 语言层面的常量概念,如文本字符串、声明为 final 的常量值等。

2、符号引用(Symbolic References)(🔥)

符号引用 则属于编译原理方面的概念,包括了 三类常量,如下所示:

  • 1)、类和接口的全限定名(Fully Qualified Name)
  • 2)、字段的名称和描述符(Descriptor))
  • 3)、方法的名称和描述符

此外,在虚拟机加载 Class 文件的时候会进行动态链接,因为其字段、方法的符号引用不经过运行期转换的话就无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建或运行时进行解析,并翻译到具体的内存地址之中

connstant_pool 中存储了一个一个的 cp_info 信息,并且每一个 cp_info 的第一个字节(即一个 u1 类型的标志位)标识了当前常量项的类型,其后才是具体的常量项内容

下面👇,我们看看有哪些具体的 常量项的类型,如下表所示:

类型标志描述
CONSTANT_Utf8_info1用于存储UTF-8编码的字符串,它真正包含了字符串的内容。
CONSTANT_Integer_info3表示int型数据的信息
CONSTANT_Float_info4表示float型数据的信息
CONSTANT_Long_info5表示long型数据的信息
CONSTANT_Double_info6表示double型数据的信息
CONSTANT_Class_info7表示类或接口的信息
CONSTANT_String_info8表示字符串,但该常量项本身不存储字符串的内容,它仅仅只存储了一个索引值
CONSTANT_Fieldref_info9字段的符号引用
CONSTANT_Methodref_info10类中方法的符号引用
CONSTANT_InterfaceMethodref_info11接口中方法的符号引用
CONSTANT_NameAndType_info12描述类的成员域或成员方法相关的信息
CONSTANT_MethodHandle_info15表示方法句柄信息,其和反射相关
CONSTANT_MethodType_info16标识方法类型,仅包含方法的参数类型和返回值类型
CONSTANT_InvokeDynamic_info18表示一个动态方法调用点,用于 invokeDynamic 指令,Java 7引入

然后,我们需要了解其中涉及到的重点常量项类型。这里我们需要先明白 CONSTANT_String 和 CONSTANT_Utf8 的区别。

CONSTANT_String 和 CONSTANT_Utf8 的区别

  • CONSTANT_Utf8真正存储了字符串的内容,其对应的数据结构中有一个字节数组,字符串便酝酿其中
  • CONSTANT_String本身不包含字符串的内容,但其具有一个指向 CONSTANT_Utf8 常量项的索引

我们必须要了解的是,在所有常见的常量项之中,只要是需要表示字符串的地方其实际都会包含有一个指向 CONSTANT_Utf8_info 元素的索引。而一个字符串最大长度即 u2 所能代表的最大值为 65536,但是需要使用 2 个字节来保存 null 值,所以一个字符串的最大长度为 65534

对于常见的常量项来说一般可以细分为如下 三个维度

常量项 Utf8

常量项 Utf8 的数据结构如下所示:

CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}

其元素含义如下所示:

  • 1)、tag值为 1,表示是 CONSTANT_Utf8_info 类型表
  • 2)、lengthlength 表示 bytes 的长度,比如 length = 10,则表示接下来的数据是 10 个连续的 u1 类型数据
  • 3)、bytesu1 类型数组,保存有真正的常量数据

常量项 Class、Filed、Method、Interface、String

常量项 Class、Filed、Method、Interface、String 的数据结构分别如下所示:

CONSATNT_Class_info {
u1 tag;
u2 name_index;
}

CONSTANT_Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}

CONSTANT_MethodType_info {
u1 tag;
u2 descriptor_index;
}

CONSTANT_InterfaceMethodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}

CONSTANT_String_info {
u1 tag;
u2 string_index;
}

CONSATNT_NameAndType_info {
u1 tag;
u2 name_index;
u2 descriptor_index
}

其元素含义如下所示:

  • name_index指向常量池中索引为 name_index 的常量表。比如 name_index = 6,表明它指向常量池中第 6 个常量

  • class_index指向当前方法、字段等的所属类的引用

  • name_and_type_index指向当前方法、字段等的名字和类型的引用

  • name_index指向某字段或方法等的名称字符串的引用

  • descriptor_index指向某字段或方法等的类型字符串的引用

常量项 Integer、Long、Float、Double

常量项 Integer、Long、Float、Double 对应的数据结构如下所示:

CONSATNT_Integer_info {
u1 tag;
u4 bytes;
}

CONSTANT_Long_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}

CONSTANT_Float_info {
u1 tag;
u4 bytes;
}

CONSTANT_Double_info {
u1 tag;
u4 high_bytes;
u4 low_bytes;
}

可以看到,在每一个非基本类型的常量项之中,除了其 tag 之外,最终包含的内容都是字符串。正是因为这种互相引用的模式,才能有效地节省 Class 文件的空间。(ps:利用索引来减少空间占用是一种行之有效的方式

三、信息描述规则

对于 JVM 来说,其 采用了字符串的形式来描述数据类型、成员变量及成员函数 这三类。因此,在讨论接下来各个的 Class 表项之前,我们需要了解下 JVM 中的信息描述规则。下面,我们来一一对此进行探讨。

1、数据类型

数据类型通常包含有 原始数据类型、引用类型(数组),它们的描述规则分别如下所示:

  • 1)、原始数据类型
  • Java 类型的 byte、char、double、float、int、long、short、boolean => "B"、"C"、"D"、"F"、"I"、"J"、"S"、"Z"
  • 2)、引用数据类型
  • ClassName => L + 全路径类名(其中的 “.” 替换为 “/”,最后加分号),例如 String => Ljava/lang/String;
  • 3)、数组(引用类型)
  • 不同类型的数组 => “[该类型对应的描述名”,例如 int 数组 => "[I",String 数组 => "[Ljava/lang/Sting;",二维 int 数组 => "[[I"

2、成员变量

在 JVM 规范之中,成员变量即 Field Descriptor 的描述规则如下所示:

FiledDescriptor:

1、仅包含 FieldType 一种信息

FieldType
FiledType:

2、FiledType 的可选类型

BaseType | ObjectType | ArrayType
BaseType:
B | C | D | F | I | J | S | Z
ObjectType:
L + 全路径ClassName;
ArrayType:
[ComponentType:

3、与 FiledType 的可选类型一样

ComponentType:
FiledType

在注释1处,FiledDescriptor 仅仅包含了 FieldType 一种信息;注释2处,可以看到,FiledType 的可选类型为3中:BaseType、ObjectType、ArrayType,对于每一个类型的规则描述,我们在 数据类型 这一小节已详细分析过了。而在注释3处,这里 ComponentType 是一种 JVM 规范中新定义的类型,不过它是 由 FiledType 构成,其可选类型也包含 BaseType、ObjectType、ArrayType 这三种。此外,对于字节码来讲,如果两个字段的描述符不一致, 那字段重名就是合法的

3、成员函数描述规则

在 JVM 规范之中,成员函数即 Method Descriptor 的描述规则如下所示:

MethodDescriptor:

1、括号内的是参数的数据类型描述,* 表示有 0 至多个 ParameterDescriptor,最后是返回值类型描述

( ParameterDescriptor* ) ReturnDescriptor
ParameterDescriptor:
FieldType
ReturnDescriptor:
FieldType | VoidDescriptor
VoidDescriptor:
// 2、void 的描述规则为 “V”
V

在注释1处,MethodDescriptor 由两个部分组成,括号内的是参数的数据类型描述,表示有 0 至多个 ParameterDescriptor,最后是返回值类型描述。注释2处,要注意 void 的描述规则为 “V”。例如,一个 void hello(String str) 的函数 => (Ljava/lang/String;)V

了解了信息的描述规则之后,我们就可以来看看 Class 表中的其它重要的表项:filed_info 与 method_info。

四、filed_info 与 method_info

字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但 不包括在方法内部声明的局部变量

filed_info 与 method_info 数据结构的伪代码分别如下所示:

field_info {
u2 access_flags;
u2 name
u2 descriptor_index
u2 attributes_count
attribute_info attributes[attributes_count]
}

method_info {
u2 access_flags;
u2 name
u2 descriptor_index
u2 attributes_count
attribute_info attributes[attributes_count]
}

可以看到,filed_info 与 method_info 都包含有 访问标志、名字引用、描述信息、属性数量与存储属性 的数据结构。对于 method_info 所描述的成员函数来说,它的内容经过编译之后得到的 Java 字节码会保存在属性之中。

注意:类构造器为 “< clinit >” 方法,而实例构造器为 “< init >” 方法

下面,我们就来了解下 access_flags 的相关知识。

五、access_flags

access_flag 的取值类型在 Class、Filed、Method 之中都是不同的,我们分别来看看。

1、Class 的 access_flags 取值类型

access_flags 中一共有 16 个标志位可以使用,当前只定义了其中 8 个(JDK 1.5 增加了后面 3 种),没有使用到的标志位要求一律为 0。Class 的 access_flags 取值类型如下表示:

标志名标志值标志含义
ACC_PUBLIC0x0001public类型
ACC_FINAL0x0010final类型
ACC_SUPER0x0020使用新的invokespecial语义
ACC_INTERFACE0x0200接口类型
ACC_ABSTRACT0x0400抽象类型
ACC_SYNTHETIC0x1000该类不由用户代码生成
ACC_ANNOTATION0x2000注解类型
ACC_ENUM0x4000枚举类型

例如一个 “public Class JsonChao” 的类所对应的 access_flags 为 0021(0X0001 和 0X0020 相结合)。下面的 Filed 与 Method 的计算也是同理。

2、Filed 的 access_flag 取值类型

接口之中的字段必须有 ACC_PUBLIC、ACC_STATIC、ACC_FINAL 标志,这些都是由 Java 本身的语言规则所决定的。Filed 的 access_flag 取值类型如下表所示:

名称描述
ACC_PUBLIC0x0001public
ACC_PRIVATE0x0002private
ACC_PROTECTED0x0004protected
ACC_STATIC0x0008static
ACC_FINAL0x0010final
ACC_VOLATILE0x0040volatile
ACC_TRANSIENT0x0080transient,不能被序列化
ACC_SYNTHETIC0x1000由编译器自动生成
ACC_ENUM0x4000enum,字段为枚举类型

3、Method 的 access_flag 取值

Method 的 access_flag 取值如下表所示:

名称描述
ACC_PUBLIC0x0001public
ACC_PRIVATE0x0002private
ACC_PROTECTED0x0004protected
ACC_STATIC0x0008static
ACC_FINAL0x0010final
ACC_SYNCHRONIZED0x0020synchronized
ACC_BRIDGE0x0040bridge,方法由编译器产生
ACC_VARARGS0x0080该方法带有变长参数
ACC_NATIVE0x0100native
ACC_ABSTRACT0x0400abstract
ACC_STRICT0x0800strictfp
ACC_SYNTHETIC0x1000方法由编译器生成

需要注意的是,当 Method 的 access_flags 的取值为 ACC_SYNTHETIC 时,该 Method 通常被称之为 合成函数。此外,当内部类访问外部类的私有成员时,在 Class 文件中也会生成一个 ACC_SYNTHETIC 修饰的函数

六、属性

只要不与已有属性名重复,任何人 实现的编译器都可以向属性表中写入自己定义的属性信息,Java 虚拟机运行时会忽略掉它所不认识的属性。

attribute_info 的数据结构伪代码如下所示:

attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}

attribute_info 中的各个元素的含义如下所示:

  • attribute_name_index为 CONSTANT_Utf8 类型常量项的索引,表示属性的名称
  • attribute_length属性的长度
  • info属性具体的内容

1、attribute_name_index

attribute_name_index 所指向的 Utf8 字符串即为属性的名称,而 属性的名称是被用来区分属性的。所有的属性名称如下所示(其中下面👇 标红的为重要属性):

  • 1)、ConstantValue仅出现在 filed_info 中,描述常量成员域的值,通知虚拟机自动为静态变量赋值。对于非 static 类型的变量(也就是实例变量)的赋值是在实例构造器方法中进行的;而对 于类变量,则有两种方式可以选择:在类构造器方法中或者使用 ConstantValue 属性。如果变量没有被 final 修饰,或者并非基本类型及字 符串,则将会选择在方法中进行初始化
  • 2)、Code仅出现 method_info 中,描述函数内容,即该函数内容编译后得到的虚拟机指令,try/catch 语句对应的异常处理表等等
  • 3)、StackMapTable在 JDK 1.6 发布后增加到了 Class 文件规范中,它是一个复杂的变长属性。这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(Type Checker)使用,目的在于代替以前比较消耗性能的基于数据流 分析的类型推导验证器。它省略了在运行期通过数据流分析去确认字节码的行为逻辑合法性的步骤,而是在编译阶 段将一系列的验证类型(Verification Types)直接记录在 Class 文件之中,通过检查这些验证类型代替了类型推导过程,从而大幅提升了字节码验证的性能。这个验证器在 JDK 1.6 中首次提供,并在 JDK 1.7 中强制代替原本基于类型推断的字节码验证器。StackMapTable 属性中包含零至多个栈映射帧(Stack Map Frames),其中的类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束
  • 4)、Exceptions当函数抛出异常或错误时,method_info 将会保存此属性
  • 5)、InnerClasses:用于记录内部类与宿主类之间的关联。
  • 6)、EnclosingMethod
  • 7)、Synthetic:标识方法或字段为编译器自动生成的。
  • 8)、SignatureJDK 1.5 中新增的属性,用于支持泛型情况下的方法签名,由于 Java 的泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息
  • 9)、SourceFile包含一个指向 Utf8 常量项的索引,即 Class 对应的源码文件名
  • 10)、SourceDebugExtension:用于存储额外的调试信息。
  • 11)、LineNumberTableJava 源码的行号与字节码指令的对应关系
  • 12)、LocalVariableTable局部变量数组/本地变量表,用于保存变量名,变量定义所在行
  • 13)、LocalVariableTypeTableJDK 1.5 中新增的属性,它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
  • 14)、Deprecated
  • 15)、RuntimeVisibleAnnotations
  • 16)、RuntimeInvisibleAnnotations
  • 17)、RuntimeVisibleParameterAnnotations
  • 18)、RuntimeInvisibleParameterAnnotations
  • 19)、AnnotationDefault
  • 20)、BootstrapMethods:JDK 1.7中新增的属性,用于保存 invokedynamic 指令引用的引导方法限定符。切记,类文件的属性表中最多也只能有一个 BootstrapMethods 属性。

在上述表格中,我们可以发现,不同类型的属性可能会出现在 ClassFile 中不同的成员里,当 JVM 在解析 Class 文件时会校验 Class 成员应该禁止携带有哪些类型的属性。此外,属性也可以包含子属性,例如:“Code” 属性中包含有 “LocalVariableTable”

2、Code_attribute(🔥)

首先,要注意 并非所有的方法表都必须存在这个属性,例如接口或者抽象类中的方法就不存在 Code 属性

Code_attribute 的数据结构伪代码如下所示:

Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{
u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}

Code_attribute 中的各个元素的含义如下所示:

  • attribute_name_index、attribute_lengthattribute_length 的值为整个 Code 属性减去 attribute_name_index 和 attribute_length 的长度
  • max_stack为当前方法执行时的最大栈深度,所以 JVM 在执行方法时,线程栈的栈帧(操作数栈,operand satck)大小是可以提前知道的。每一个函数执行的时候都会分配一个操作数栈和局部变量数组,而 Code_attribure 需要包含它们,以便 JVM 在执行函数前就可以分配相应的空间
  • max_locals:**为当前方法分配的局部变量个数,包括调用方式时传递的参数。long 和 double 类型计数为 2,其他为 1。max_locals 的单位是 Slot,Slot 是

虚拟机为局部变量分配内存所使用的最小单位。局部变量表中的 Slot 可以重用,当代码执行超出一个局部变量的作用域时,这个局部变量 所占的 Slot 可以被其他局部变量所使用,Javac 编译器会根据变量的作用域来分配 Slot 给各个 变量使用,然后计算出 max_locals 的大小**。

  • code_length为方法编译后的字节码的长度
  • code用于存储字节码指令的一系列字节流。既然叫字节码指令,那么每个指令就是一个 u1 类型的单字节。一个 u1 数据类型的取值范围为 0x00~0xFF,对应十进制的 0~255,也就是一共可以表达 256 条指令
  • exception_table_length表示 exception_table 的长度
  • exception_table每个成员为一个 ExceptionHandler,并且一个函数可以包含多个 try/catch 语句,一个 try/catch 语句对应 exception_table 数组中的一项
  • start_pc、end_pc为异常处理字节码在 code[] 的索引值。当程序计数器在 [start_pc, end_pc) 内时,表示异常会被该 ExceptionHandler 捕获
  • handler_pc表示 ExceptionHandler 的起点,为 code[] 的索引值
  • catch_type为 CONSTANT_Class 类型常量项的索引,表示处理的异常类型。如果该值为 0,则该 ExceptionHandler 会在所有异常抛出时会被执行,可以用来实现 finally 代码。当 catch_type 的值为 0 时,代表任意异常情况都需要转向到 handler_pc 处进行处理。此外,编译器使用异常表而不是简单的跳转命令来实现 Java 异常及 finally 处理机制
  • attributes_count 和 attributes表示该 exception_table 拥有的 attribute 数量与数据

在 Code_attribute 携带的属性中,"LineNumberTable""LocalVariableTable" 对我们 Android 开发者来说比较重要,所以,这里我们将再单独来讲解一下它们。

1)、LineNumberTable 属性

LineNumberTable 属性 用于 Java 的调试,可指明某条指令对应于源码哪一行

LineNumberTable 属性的结构如下所示:

LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{ u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length];
}

其中最重要的是 line_number_table 数组,该数组元素包含如下 两个成员变量

  • 1、start_pc为 code[] 数组元素的索引,用于指向 Code_attribute 中 code 数组某处指令
  • 2、line_number为 start_pc 对应源文件代码的行号。需要注意的是,多个 line_number_table 元素可以指向同一行代码,因为一行 Java 代码很可能被编译成多条指令

2、LocalVariableTable 属性

LocalVariableTable 属性用于 描述栈帧中局部变量表中的变量与 Java 源码中定义的变量之间的关系,它也不是运行时必需的属性,但默认会生成到 Class 文件之中。

LocalVariableTable 的数据结构如下所示:

LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{
u2 start_pc;
u2 length;
u2 name_index;
u2 descriptor_index;
u2 index;
} local_variable_table[local_variable_table_length];
}

其中最重要的元素是 local_variable_table 数组,其中的 start_pclength 这两个参数 决定了一个局部变量在 code 数组中的有效范围

需要注意的是,每个非 static 函数都会自动创建一个叫做 this 的本地变量,代表当前是在哪个对象上调用此函数。并且,this 对象是位于局部变量数组第1个位置(即 Slot = 0),它的作用范围是贯穿整个函数的

此外,在 JDK 1.5 引入泛型之后,LocalVariableTable 属性增加了一个 “姐妹属性”: LocalVariableTypeTable,这个新增的属性结构与 LocalVariableTable 非常相似,仅仅是把记录 的字段描述符的 descriptor_index 替换成了字段的特征签名(Signature),对于非泛型类型来 说,描述符和特征签名能描述的信息是基本一致的,但是泛型引入之后,由于描述符中泛型的参数化类型被擦除掉,描述符就不能准确地描述泛型类型了,因此出现了 LocalVariableTypeTable

Slot 是什么?

JVM 在调用一个函数的时候,会创建一个局部变量数组(即 LocalVariableTable),而 Slot 则表示当前变量在数组中的位置

七、JVM 指令码(🔥)

在上面,我们了解了 常量池、属性、field_info、method_info 等等一系列的源码文件组成结构,它们是仅仅是一种静态的内容,这些信息并不能驱使 JVM 执行我们在源码中编写的函数

从前可知,Code_attribute 中的 code 数组存储了一个函数源码经过编译后得到的 JVM 字节码,其中仅包含如下 两种 类型的信息:

  • 1)、JVM 指令码用于指示 JVM 执行的动作,例如加操作/减操作/new 对象。其长度为 1 个字节,所以 JVM 指令码的个数不会超过 255 个(0xFF)
  • 2)、JVM 指令码后的零至多个操作数操作数可以存储在 code 数组中,也可以存储在操作数栈(Operand stack)中

一个 Code 数组里指令和参数的组织格式 如下所示:

1字节指令码 0或多个参数(N字节,N>=0)

可以看到,Java 虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作 码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。此外,大多数的指令都不包含操作数,只有一个操作码

字节码指令集是一种具有鲜明特点、优劣势都很突出的指令集架构,由于限制了 Java 虚拟机操作码的长度为一个字节(即 0~255),这意味着指令集的操作码总数不可能超过 256 条

如果不考虑异常处理的话,那么 Java 虚拟机的解释器可以使用下面这个伪代码当做 最基本的执行模型 来理解,如下所示:

do {
自动计算PC寄存器的值加1;
根据PC寄存器的指示位置,从字节码流中取出操作码;
if(字节码存在操作数)从字节码流中取出操作数;
执行操作码所定义的操作;
} while (字节码流长度>0);

由于 Java 虚拟机的操作码长度只有一个字节,所以,Java 虚拟机的指令集 对于特定的操作只提供了有限的类型相关指令去支持它。例如 在 JVM 中,大部分的指令都没有支持整数类型 byte、char 和 short,甚至没有任何指令支持 boolean 类型。因此,我们在处理 boolean、byte、short 和 char 类型的数组时,需要转换为与之对应的 int 类型的字节码指令来处理

众所周知,JVM 是基于栈而非寄存器的计算模型,并且,基于栈的实现能够带来很好的跨平台特性,因为寄存器指令往往和硬件挂钩。但是,由于栈只是一个 FILO 的结构,需要频繁地压栈与出栈,因此,对于同样的操作,基于栈的实现需要更多指令才能完成。此外,由于 JVM 需要实现跨平台的特性,因此栈是在内存实现的,而寄存器则位于 CPU 的高速缓存区,因此,基于栈的实现其速度速度相比寄存器的实现要慢很多。要深入了解 JVM 的指令集,我们就必须先从 JVM 运行时的栈帧讲起

1、运行时的栈帧

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟 机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素

栈帧中存储了方法的 局部变量表、操作数栈、动态连接和方法返回地址、帧数据区 等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程

一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。对于 JVM 的执行引擎来 说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(Current Stack Frame),与这个栈帧相关联的方法称为当前方法(Current Method)。执行引擎运行的所有 字节码指令都只针对当前栈帧进行操作,而 栈帧的结构 如下图所示:

Java 中当一个方法被调用时会产生一个栈帧(Stack Frame),而此方法便位于栈帧之内。而Java方法栈帧 主要包括三个部分,如下所示:

  • 1)、局部变量区
  • 2)、操作数栈区
  • 3)、帧数据区(常量池引用)

帧数据区,即常量池引用在前面我们已经深入地了解过了,但是还有两个重要部分我们需要了解,一个是操作数栈,另一个则是局部变量区。通常来说,程序需要将局部变量区的元素加载到操作数栈中,计算完成之后,然后再存储回局部变量区

查看字节码的工具

我们可以使用 jclasslib 这个字节码工具去查看字节码,使用效果如下图所示,代码编译后在菜单栏 ”View” 中选择 ”Show Bytecode With jclasslib”,可以很直观地看到当前字节码文件的类信息、常量池、方法区等信息

下面👇,我们就先来看看操作数栈是怎么运转的。

2、操作数栈

操作数栈是为了 存放计算的操作数和返回结果。在执行每一条指令前,JVM 要求该指令的操作数已经被压入到操作数栈中,并且,在执行指令时,JVM 会将指令所需的操作数弹出,并将计算结果压入操作数栈中

对于操作数栈相关的操作指令有如下 三类

1)、直接作用于操作数据栈的指令:

  • dup复制栈顶元素,常用于复制 new 指令所生成的未初始化的引用
  • pop舍弃栈顶元素,常用于舍弃调用指令的返回结果
  • wap交换栈顶的两个元素的值

需要注意的是,当值为 long 或 double 类型时,需要占用两个栈单元,此时需要使用 dup2/pop2 指令替代 dup/pop 指令。

2)、直接将常量加载到操作数栈的指令:

对于 int(boolean、byte、char、short) 类型来说,有如下三类常用指令:

  • iconst用于加载 [-1 ,5] 的 int 值
  • biconst用于加载一个字节(byte)所能代表的 int 值即 [-128-127]
  • sipush用于加载两个字节(short)所能代表的 int 值即 [-32768-32767]

而对于 long、float、double、reference 类型来说,各个类型都仅有一类,其实就是类似于 iconst 指令,即 lconst、fconst、dconst、aconst

3)、加载常量池中的常量值的指令:

  • ldc用于加载常量池中的常量值,如 int、long、float、double、String、Class 类型的常量。例如 ldc #35 将加载常量池中的第 35 项常量值

正常情况下,操作数栈的压入弹出都是一条条指令完成。唯一的例外是在抛异常时,JVM 会清除操作数栈的所有内容,然后将异常实例压入操作数栈中

3、局部变量区

局部变量区一般用来 缓存计算的结果。实际上,JVM 会把局部变量区当成一个 数组,里面会依次缓存 this 指针(非静态方法)、参数、局部变量

需要注意的是,同操作数栈一样,long 和 double 类型的值将占据两个单元,而其它的类型仅仅占据一个单元

而对于局部变量区来说,它常用的操作指令有 三种,如下所示:

1)、将局部变量区的值加载到操作数栈中

  • int(boolean、byte、char、short)iload
  • longlload
  • floatfload
  • doubledload
  • referenceaload

2)、将操作数栈中的计算结果存储在局部变量区中

  • int(boolean、byte、char、short)istore
  • longlstore
  • floatfstore
  • doubledstore
  • referenceastore

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

结尾

我还总结出了互联网公司Android程序员面试涉及到的绝大部分面试题及答案,并整理做成了文档,以及系统的进阶学习视频资料分享给大家。
(包括Java在Android开发中应用、APP框架知识体系、高级UI、全方位性能调优,NDK开发,音视频技术,人工智能技术,跨平台技术等技术资料),希望能帮助到你面试前的复习,且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。

image

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

78954582)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

结尾

我还总结出了互联网公司Android程序员面试涉及到的绝大部分面试题及答案,并整理做成了文档,以及系统的进阶学习视频资料分享给大家。
(包括Java在Android开发中应用、APP框架知识体系、高级UI、全方位性能调优,NDK开发,音视频技术,人工智能技术,跨平台技术等技术资料),希望能帮助到你面试前的复习,且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。

[外链图片转存中…(img-azTaojVv-1713678954584)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 20
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值