文章大纲
引言
对于我们Java 程序员来说,或许对于Java源文件,再熟悉不过了,毕竟整天都是与之打交道,但是是否是真的已经很熟悉了,镇的了解了吗?我想大家都知道Java 源文件经过编译之后转为.class字节码文件,但是是否知道所谓的插桩本质上就是操作字节码呢?如果你想学习插桩那么这个是必须要掌握的技术基础之一。
一、字节码指令概述
Java跨平台的本质就是中间产物.class,Java文件经过javac编译后生成字节码文件(.class),再由JVM通过对字节码指令的解释执行,从而屏蔽对操作系统的依赖。一个字节8位可以存储256种不同的指令,Java虚拟机规范约定了不同的取值代表不同的含义,可以被Java虚拟机解释执行,这些指令即字节码指令,个人理解字节码指令是特定为Java虚拟机去定义了的一套“解析”指令,类似汇编的作用,只不过去解析执行这套指令的对象和环境不同(仅供参考)
1、加载或储存指令
在栈帧中,通过指令操作数据在局部变量表与操作栈间传递。
- ILOAD、ALOAD——将int、对象引用类型从局部变量表压入操作栈顶;
- ISTORE、ASTORE——将int、对象引用类型从操作栈顶储存到局部变量表里;
- ICONST、BIPUSH、SIPUSH、LDC——将常亮加载到操作栈顶。
2、运算指令
对操作栈上的值进行运算,并把结果写入操作栈顶,如IADD、IMUL、SUB。
3、类型转换指令
I2L、D2F等
4、对象创建与访问指令
NEW
除了字节码指令外,还包括像LINENUMBER储存字节码与Java源文件对应的行号方便调试定位;LOCALVARIABLE储存当前方法使用到的局部表量表,欲了解更多可以去阅读Java虚拟机规范文档。
二、字节码文件(.class)
1、字节码文件结构概述
*代表出现0或者多次
Java源文件经过javac 编译得到字节码文件,核心 API是用于生成和转换经过编译的字节码文件的。与原生的编译应用程序不同,如上图所示class类文件中保留了来自Java源代码文件的结构信息和几乎所有符号,大体上包含以下几个部分:
- 描述类的修饰符(比如 public 和 private)、名字、超类、接口等信息。
- 类中声明的每个字段及对应的描述每个字段的修饰符、 名字、 类型和描述等。
- 类中声明的每个方法、构造器的部分以及每一部分描述一个方法的修饰符、名字、返回类型与参数类型、方法签名、方法等信息,其中方法体是 Java 字节代码指令的形式的体现的。
不过,源文件和字节码文件内部还是存在一些差异的:
-
一个字节码文件仅能描述一个类,而一个源文件中可以则包含几个类。当一个源文件是描述一个包含内部类的类,那么该源文件则会被编译为两个类文件(主类和内部类各一个文件),主类文件中包含对其内部类的引用,定义了内部方法的内部类会包含外部引用
-
字节码文件中不包含注释( comment),但可以包含类、字段、方法和代码属性,可
以利用这些属性为相应元素关联更多信息。 -
字节码文件中不包含 package 和 import 部分, 所有类型名字都必须是完全限定的。
-
字节码文件还包含常量池(constant pool),所谓常量池本质上就是一个数组存储了类中出现的所有数值、字符串和类型常量,这些常量仅需要在这个常量池部分中定义一次,就可以利用其索引,在类文件中的所有其他各部分进行引用。
2、class文件浅析
Java class类文件是 8 位字节的二进制流,数据项按顺序存储在 class 文件中,相邻的项之间没有间隔,这使得 class 文件变得紧凑,减少存储空间,而Java 字节码文件按照Java 虚拟机规范中的格式对字节码指令进行组织并存储的结构化文件,可以把Java文件看成规范的结构文档,下图是一个class文件的内部16进制字节结构:
Java class文件的数据结构如下所示:
由图得知,每一个Java class文件内部都由以下十个元素项构成:
元素项 | 长度(字节) | 说明 |
---|---|---|
Magic | 4 | 该项存放了一个 Java 类文件的魔数(magic number,一个 Java 类文件的前 4 个字节被称为它的魔数。每个正确的 Java 类文件都是以 0xCAFEBABE 开头的,这样保证了 Java 虚拟机能很轻松的分辨出 Java 文件和非 Java 文件,从而进一步确保文件的安全性。 |
Version | 2+2 | 该项存放了 Java 类文件的版本信息,其中前两个字节为次版本号,后两个字节为主版本号(以本文的为例次本版号十进制为0,主版本号为52对应J2SE8,于是得到该文件的版本为1.8.0),因为 Java 技术一直在发展,所以类文件的格式也处在不断变化之中。类文件的版本信息让虚拟机知道如何去读取并处理该类文件。 |
Constant Pool | 2+n | 常量池是Class文件中的资源仓库,主要存储2大类常量——字面量(如文本字符串,java中声明为final的常量值等)和符号引用(如类和接口的全局限定名,字段的名称和描述符,方法的名称和描述符)。虚拟机必须为每一个被装载的类维护一个常量池,常量池中存储了相应类型所用到的所有类型、字段和方法的符号引用,因此它在 Java 的动态链接中起到了核心的作用。常量池的大小平均占到了整个类大小的 60% 左右。 |
Access Flags | 2 | 该项指明了该文件中定义的是类还是接口(一个 class 文件中只能有一个类或接口),同时还指名了类或接口的访问标志,如 public,private, abstract 等信息。 |
This Class Name | 2 | 指向表示该类全限定名称的字符串常量的指针。 |
Super Class Name | 2 | 指向表示父类全限定名称的字符串常量的指针。 |
Interfaces | 2+n | 一个指针数组,存放了该类或父类实现的所有接口名称的字符串常量的指针。以上三项所指向的常量,特别是前两项,在我们用 ASM 从已有类派生新类时一般需要修改 |
Fields | 2+n | 该项对类或接口中声明的字段进行了细致的描述。需要注意的是,fields 列表中仅列出了本类或接口中的字段,并不包括从超类和父接口继承而来的字段。 |
Methods | 2+n | 该项对类或接口中声明的方法进行了细致的描述。例如方法的名称、参数和返回值类型等。需要注意的是,methods 列表里仅存放了本类或本接口中的方法,并不包括从超类和父接口继承而来的方法。使用 ASM 进行 AOP 编程,通常是通过调整 Method 中的指令来实现的。 |
Class attributes | 2+n | 该项存放了在该文件中类或接口所定义的属性的基本信息。 |
3、类内部名(类完全限定名)
通常一种类型只能是类或接口类型。例如一个类的超类、由一个类实现的接口或者由一个方法抛出的异常就不能是基元类型或数组类型,必须是类或接口类型。这些类型在字节码中用内部名字表示,一个类的内部名即类的完全限定名(其中的点号用斜线代替,String 的内部名为 java/lang/String)
4、类型描述符
内部名只能用于类或接口类型,而其他的Java 类型则使用类型描述符来描述,比如字段类型在字节码文件中都是用类型描述符表示的。
变量类型 | 类型描述符 | 包装类 | 包装类类型描述符(包含分号) |
---|---|---|---|
int | I(大写i) | Integer | Ljava/lang/Integer; |
short | S | Short | Ljava/lang/Short; |
long | J | Long | Ljava/lang/Long; |
boolean | Z | Boolean | Ljava/lang/Boolean; |
char | C | Character | Ljava/lang/Character; |
byte | B | Byte | Ljava/lang/Byte; |
float | F | Float | Ljava/lang/Float; |
double | D | Double | Ljava/lang/Double; |
void | V | Void | Ljava/lang/Void; |
Object | L+类名(使用’/'作为分隔符)+;如: Ljava/lang/Object;Lorg/objectweb/asm/MethodVisitor; | / | / |
String | Ljava/lang/String; | / | / |
-------------- | 数组写法: | -------- | ---------------- |
X的N维数组 | N个[+X的类型描述符 | / | / |
int[] | [I(大写的i) | / | / |
byte[][] | [[B | / | / |
String[] | [Ljava/lang/String; | / | / |
Object[][] | [[Ljava/lang/Object; | / | / |
如上表所示基元类型的描述符是单个字符( Z 表示 boolean、 C 表示 char等);一个类的类型的描述符是这个类的内 部 名 , 前 面 加 上 字 符 L , 后 面 跟 有 一 个 分 号 ( String 的 类 型 描 述 符 为Ljava/lang/String;);而一个数组类型的描述符是一个方括号后面跟有该数组元素类型的描述符。
5、方法描述符(方法签名)
方法描述符(方法签名)是一个类型描述符列表,它用一个字符串描述一个方法的参数类型和返回类型。方法描述符以左括号开头,然后是每个形参的类型描述符,然后是一个右括号,接下来是返回类型的类型描述符,如果该方法返回 void,则是 V(方法描述符中不包含方法的名字或参数名)。
源文件中的方法声明 | 方法描述符 | 说明 |
---|---|---|
void m(int i, float f) | (IF)V | 接收一个int和float型参数且无返回值 |
int m(Object o) | (Ljava/lang/Object;)I | 接收Object型参数返回int |
int[] m(int i, String s) | (ILjava/lang/String;)[I | 接受int和String返回一个int[] |
Object m(int[] i) | ([I)Ljava/lang/Object; | 接受一个int[]返回Object |
6、java源码文件转化为字节码文件(.class)的过程概述
java源码文件转化为字节码文件主要有6个核心步骤:
7、执行模式概述
-
解释执行——JVM通过加载到的字节码进行执行。
-
JIT编译执行——将热点代码(例如:高频方法体、循环体、公共模块)直接翻译成机器码,提高以后的执行效率。
-
JIT编译执行与解释执行混合执行(主流JVM执行模式)——每次方法调用的时候,方法调用计数器加1,如果计数达到阈值,请求编译成机器码,将机器码放在Code cache里面,下次执行查看是否已编译成机器码,已编译的直接执行机器码,没有编译的通过解释执行(即执行字节码)。
关于Java class 和Java 虚拟机的更多知识请自行了解和学习(也可以去查找第三方分析字节码的工具),以上部分理论来自于《深入理解Java虚拟机》一书。