ASM

ClassReader类负责“读”Class,ClassWriter负责“写”Class,而ClassVisitor则负责进行“转换”(Transformation)。在Class Transformation过程中,可以有多个ClassVisitor参与。不过要注意,ClassVisitor类是一个抽象类,我们需要写代码来实现一个ClassVisitor类的子类才能使用。
类结构


Java类型描述符
| Java类型 | 描述 |
|---|---|
| boolean | Z |
| char | C |
| byte | B |
| int | I |
| float | F |
| double | D |
| long | J |
| Object | Ljava/lang/Object; |
| int[] | [I |
| Object[] | [[Ljava/lang/Object; |
核心类
1. ClassVisitor.class
1 | public abstract class ClassVisitor { |
2. FieldVisitor.class
1 | public abstract class FieldVisitor { |
3. MethodVisitor.class
可以生成一个.class文件当中各个部分的内容。
1 | public class HelloWorld { |
在这里,我们只关心方法的部分:
- 对于方法头的部分,我们可以使用
ClassVisitor.visitMethod(int access, String name, String descriptor, String signature, String[] exceptions)方法来提供。- 其中的
access参数提供访问标识信息,例如public - 其中的
name参数提供方法的名字,例如test - 其中的
descriptor参数提供方法的参数类型和返回值的类型
- 其中的
- 对于方法体的部分,我们可能通过使用
MethodVisitor类来实现。- 如何得到一个
MethodVisitor对象呢?ClassVisitor.visitMethod()的返回值是一个MethodVisitor类型的实例。 - 方法体的
instructions是如何添加的呢?通过调用MethodVisitor.visitXxxInsn()方法来提供的
- 如何得到一个
对于MethodVisitor类来说,我们从两个方面来把握:
第一方面,就是
MethodVisitor类的visitXxx()方法的调用顺序。这些方法的调用顺序,可以记忆如下:
- 第一步,调用visitCode()方法,调用一次。
- 第二步,调用visitXxxInsn()方法,可以调用多次。对这些方法的调用,就是在构建方法的“方法体”。
- 第三步,调用visitMaxs()方法,调用一次。
- 第四步,调用visitEnd()方法,调用一次。
第二方面,就是
MethodVisitor类的visitXxxInsn()方法具体有哪些。- visitInsn(int):访问一个零参数要求的字节码指令,如ACONST_NULL
- visitIntInsn(int, int):访问一个需要零操作栈要求但需要有一个int参数的字节码指令,如BIPUSH
- visitVarInsn(int, int):访问一个有关于局部变量的字节码指令,如ALOAD
- visitTypeInsn(int, String):访问一个有关于类型的字节码指令,如CHECKCAST
- visitFieldInsn(int, String, String, String):访问一个有关于字段的字节码,如PUTFIELD
- visitMethodInsn(int, String, String, String, boolean):访问一个有关于方法调用的字节码,如INVOKESPECIAL
- visitJumpInsn(int, Label):访问跳转字节码,如IFEQ之后,是一些被包装好的字节码访问方法,这些方法都基于最基本的字节码指令,但是不需要我们自己用上面提到的那些方法直接调用字节码。
- visitInvokeDynamicInsn(String, String, Handle, Object…):基于INVOKEDYNAMIC,动态方法调用,会在lambda表达式和方法引用里面说到
- visitLdcInsn(Object):基于LDC、LDC_W和LDC2_W,将一个常量加载到操作栈用(详细见下文)
- visitIincInsn(int, int):基于IINC、IINC_W,自增/减表达式
- visitTableSwitchInsn(int, int, Label, Label…):基于TABLESWITCH,用于进行table-switch操作
- visitLookupSwitchInsn(Label, int[], Label[]):基于LOOKUPSWITCH,用于进行lookup-switch操作
- visitMultiANewArrayInsn(String, int):基于MULTIANEWARRAY,用于创建多重维度数组,如int[][]
方法字段解释
1. visit()
访问类的声明部分
1 | public class visit( |
2. visitSource()
访问类代码
1 | void visitSource(String source, String debug); |
3. visitOuterClass()
访问外部类
1 | void visitOuterClass(String owner, String name, String desc); |
4. visitAnnotation()
访问类的注解
1 | AnnotationVisitor visitAnnotation(String desc, boolean visible); |
5. visitAttribute()
访问类的属性
1 | void visitAttribute(Attribute attr); |
6. visitInnerClass()
访问类的内部类
1 | void visitInnerClass(String name, String outerName, String innerName,int access); |
7. visitField()
访问类的字段
1 | public FieldVisitor visitField( |
8. visitMethod()
用于填充类的方法表信息
1 | public MethodVisitor visitMethod( |
9.visitEnd()
1 | public void visitEnd() |
Java方法描述符
| 方法声明 | 方法描述符 |
|---|---|
| void m(int i, float f) | (IF)V |
| int m(Object o) | (Ljava/lang/Object;)I |
| int[] m(int i, String s) | (ILjava/lang/String;)[I |
| Object m(int[] i) | ([I)Ljava/lang/Object; |
- 接口和组件
1. 解析类
在分析一个已经存在的类时,惟一必需的组件是 ClassReader 组件。让我们用一个例子 来 说明。假设希望打印一个类的内容,其方式类似于 javap 工具。第一步是编写 ClassVisitor 类的一个子类,打印它所访问的类的相关信息。下面是一种可能的实现方式,它有些过于简化了:
1 | public class ClassPrinter extends ClassVisitor { |
第二步是将这个 ClassPrinter 与一个 ClassReader 组件合并在一起,使 ClassReader 产生的事件由我们的 ClassPrinter 使用:
1 | ClassPrinter cp = new ClassPrinter(); |
第二行创建了一个 ClassReader,以分析 Runnable 类。在最后一行调用的 accept 方
法分析 Runnable 类字节代码,并对 cp 调用相应的 ClassVisitor 方法。结果为以下输出:
1 | // 真实的Runnable接口也只有一个方法 void run(),和这个输出对应 |
注意,构建 ClassReader 实例的方式有若干种。必须读取的类可以像上面一样用名字指定, 也 可 以 像 字 母 数 组 或 InputStream 一 样 用 值 来 指 定 。 利 用 ClassLoader 的getResourceAsStream 方法,可以获得一个读取类内容的输入流,如下:cl.getResourceAsStream(classname.replace(’.’, ’/’) + ".class");
2. 生成类
为生成一个类,惟一必需的组件是 ClassWriter 组件。让我们用一个例子来进行说明。 考虑以下接口:
1 | package pkg; |
可以对 ClassVisitor 进行六次方法调用来生成它:
1 | ClassWriter cw = new ClassWriter(0); |
第一行创建了一个 ClassWriter 实例,它实际上将创建类的字节数组表示(构造器参数 在下一章解释)。
对 visit 方法的调用定义了类的标头。V1_5 参数1是一个常数,与所有其他 ASM 常量一样,在 ASM Opcodes 接口中定义。它指明了类的版本——Java 1.5。ACC_XXX 常量2是与 Java 修饰 符对 应的标志。这里规定这个类是一个接口,而且它是 public 和 abstract 的(因为它不能 被实例化)。下一个3 参数以内部形式规定了类的名字。回忆一下,已编译类不包含 Package 和 Import 部分,因此,所有类名都必须是完全限定的。下一个参数4对应于泛型。在我们的例子中,这个参数是 null,因为这个接口并没有由类型变量进行参数化。 第 五5 个参数是内部形式的超类(接口类隐式继承自 Object)。最后一个参数是一个数组,其 中是 被扩展的接口,这些接口由其内部名指定。
一个完整的类定义 public class test
extend Object implent A , B { }五个参数对应五个
接下来对 visitField 的三次调用是类似的,用于定义三个接口字段。第一个参数是一组 标志,对应于 Java 修饰符。这里规定这些字段是 public、final 和 static 的。第二个参数 是字段的名字,与它在源代码中的显示相同。第三个参数是字段的类型,采用类型描述符形式。 这 里,这些字段是 int 字段,它们的描述符是 I。第四个参数对应于泛型。在我们的例子中, 它是 null,因为这些字段类型没有使用泛型。最后一个参数是字段的常量值:这个参数必须仅用于真正的常量字段,也就是 final static 字段。对于其他字段,它必须为 null。由于此处 没有注释, 所以立即调用所返回的 FieldVisitor 的 visitEnd 方法, 即对其 visitAnnotation 或 visitAttribute 方法没有任何调用。
visitMethod 调用用于定义 compareTo 方法,同样,第一个参数是一组对应于 Java 修饰符的标志。第二个参数是方法名,与其在源代码中的显示一样。第三个参数是方法的描述符。第 四 个参数对应于泛型。在我们的例子中,它是 null,因为这个方法没有使用泛型。最后一个参 数是 一个数组,其中包括可由该方法抛出的异常,这些异常由其内部名指明。它在这里为 null, 因为这个方法没有声明任何异常。visitMethod 方法返回 MethodVisitor ,可用 于定义该方法的注释和属性,最重要的是这个方法的代码。这里,由于没有注释,而且这个方法 是抽象的,所以我们立即调用所返回的 MethodVisitor 的 visitEnd 方法。
类方法或者filed等结束后还有注释,使用visitEnd代表着结束。
对 visitEnd 的最后一个调用是为了通知 **cw:**这个类已经结束,对 toByteArray 的调用用于以字节数组的形式提取它。
2.1 使用生成的类
前面的字节数组可以存储在一个 Comparable.class 文件中,供以后使用。或者,也可 以 用 ClassLoader 动态加载它。一种方法是定义一个 ClassLoader 子类,它的 defineClass 方法是公有的:
1 | class MyClassLoader extends ClassLoader { |
然后,可以用下面的代码直接调用所生成的类:
1 | Class c = myClassLoader.defineClass("pkg.Comparable", b); |
另一种加载已生成类的方法可能更清晰一些,那就是定义一个 ClassLoader 子类,它 的 findClass 方法被重写,以在运行过程中生成所请求的类:
1 | class StubClassLoader extends ClassLoader { |
事实上,所生成类的使用方式取决于上下文,这已经超出了 ASM API 的范围。如果你正 在 编写编译器,那类生成过程将由一个抽象语法树驱动,这个语法树代表将要编译的程序,而 生成 的类将被存储在磁盘上。如果你正在编写动态代理类生成器或方面编织器,那将会以这种 或那种 方式使用一个 ClassLoader。
3. 转换(修改)类
到目前为止,ClassReader 和 ClassWriter 组件都是单独使用的。这些事件是“人工” 产生,并且由 ClassWriter 直接使用,或者与之对称地,它们由 ClassReader 产生,然后 “人工”使用,也就是由自定义的 ClassVisitor 实现使用。当这些组件一同使用时,事情开始变得真正有意义起来。第一步是将 ClassReader 产生的事件转给ClassWriter 。其结果是, 类编写器重新构建了由类读取器分析的类:
1 | byte[] b1 = ...; |
这本身并没有什么真正的意义(还有其他更简单的方法可以用来复制一个字节数组!), 下一步是在类读取器和类写入器之间引入一个 ClassVisitor:
1 | byte[] b1 = ...; |
与上述代码相对应的体系结构,其中的组件用方框表示,事件用箭头表示(其中的垂直时间线与程序图中一样)。
但结果并没有改变,因为 ClassVisitor 事件筛选器没有筛选任何东西。但现在,为了能 够转换一个类,只需重写一些方法,筛选一些事件就足够了。例如,考虑下面的 ClassVisitor 子类:
1 | public class ChangeVersionAdapter extends ClassVisitor { |
这个类仅重写了 ClassVisitor 类的一个方法。结果,所有调用都被不加改变地转发到传 送给构造器的类访问器 cv,只有对 visit 方法的调用除外,在转发它时,对类版本号进行了修改。

通过修改 visit 方法的其他参数,可以实现其他转换,而不仅仅是修改类的版本。例如, 可以向实现接口的列表中添加一个接口。还可以改变类的名字,但进行这种改变所需要做的工作 要多得多,不只是改变 visit 方法的 name 参数了。实际上,类的名字可以出现在一个已编译 类的许多不同地方,要真正实现类的重命名,必须修改类中出现的所有这些类名字。
3.1 改进
前面的转换只修改了原类的四个字节。但是,在使用上面的代码时,整个 b1 均被分析,并利用相应的事件从头从头构建了 b2,这种做法的效率不是很高。如果将 b1 中不被转换的部分 直 接复制到 b2 中,不对其分析,也不生成相应的事件,其效率就会高得多。ASM 自动为方法 执行这一优化:
- 在 ClassReader 组件的 accept 方法参数中传送了 ClassVisitor , 如果 ClassReader 检测到这个 ClassVisitor 返回的 MethodVisitor 来自一个ClassWriter,这意味着这个方法的内容将不会被转换,事实上,应用程序甚至不会 看到其内容。
- 在这种情况下,ClassReader 组件不会分析这个方法的内容,不会生成相应事件, 只 是复制 ClassWriter 中表示这个方法的字节数组。
如果 ClassReader 和 ClassWriter 组件拥有对对方的引用,则由它们进行这种优化, 可设置如下:
1 | // 和前面相比,这里是互相拥有,前面是单一拥有 |
3.2 优缺点
执行这一优化后,由于 ChangeVersionAdapter 没有转换任何方法,所以以上代码的速度可以达到之前代码的两倍。对于转换部分或全部方法的常见转换,这一速度提升幅度可能要 小 一些,但仍然是很可观的:实际上在 10%到 20%的量级。遗憾的是,这一优化需要将原类中 定义的所有常量都复制到转换后的类中。对于那些增加字段、方法或指令的转换来说,这一点不成问题,但对于那些要移除或重命名许多类成员的转换来说,这一优化将导致类文件大于未优 化 时的情况。因此,建议仅对“增加性”转换应用这一优化。
3.3 使用转换后的类
如上节所述,转换后的类 b2 可以存储在磁盘上,或者用 ClassLoader 加载。但在ClassLoader 中执行的类转换只能转换由这个类加载器加载的类。如果希望转换所有类,则必 须将转换放在 ClassFileTransformer 内部,见 java.lang.instrument 包中的定义(更 多细节,请参阅这个软件包的文档):
1 | public static void premain(String agentArgs, Instrumentation inst) { |
4. 移除类成员
上一节用于转换类版本的方法当然也可用于 ClassVisitor 类的其他方法。例如,通过 改 变 visitField 和 visitMethod 方法的 access 或 name 参数,可以改变一个字段 或一个方 法的修饰字段或名字。另外,除了在转发的方法调用中使用经过修改的参数之外,还 可以选择根 本不转发该调用。其效果就是相应的类元素被移除。
例如,下面的类适配器移除了有关外部类及内部类的信息,还删除了一个源文件的名字,也就是由其编译这个类的源文件(所得到的类仍然具有全部功能,因为删除的这些元素仅用于调 试 目的)。这一移除操作是通过在适当的访问方法中不转发任何内容而实现的:
1 | public class RemoveDebugAdapter extends ClassVisitor { |
这一策略对于字段和方法是无效的,因为 visitField 和 visitMethod 方法必须返回一 个结果。要移除字段或方法,不得转发方法调用,并向调用者返回 null。例如,下面的类适配 器移除了一个方法,该方法由其名字及描述符指明(仅使用名字不足以标识一个方法,因为一个 类中可能包含若干个具有不同参数的同名方法):
1 | public class RemoveMethodAdapter extends ClassVisitor { |
5. 添加类成员
上述讨论的是少转发一些收到的调用,我们还可以多“转发”一些调用,也就是发出的调用 数多于收到的调用,其效果就是增加了类成员。新的调用可以插在原方法调用之间的若干位置, 只要遵守各个 visitXxx 必须遵循的调用顺序即可。
例如,如果要向一个类中添加一个字段,必须在原方法调用之间添加对 visitField 的一 个新调用,而且必须将这个新调用放在类适配器的一个访问方法中。比如,不能在 visit 方法 中 这样做, 因为这样可能会导致对 visitField 的调用之后跟有visitSource 、 visitOuterClass、visitAnnotation 或 visitAttribute,这是无效的。出于同样的原 因,不能将这个新调用放在 visitSource、visitOuterClass、visitAnnotation 或 visitAttribute 方法中,仅有的可能位置是 visitInnerClass 、 visitField 、 visitMethod 或 visitEnd 方法。
如果将这个新调用放在 visitEnd 方法中,那这个字段将总会被添加(除非增加显式条件), 因为这个方法总会被调用。如果将它放在 visitField 或 visitMethod 中,将会添加几个字 段:原类中的每个字段和方法各有一个相应的字段。这两种解决方案都可能发挥应有的作用;具 体取决于你的需求。例如,可以仅添加一个计数器字段,用于计算对一个对象的调用次数,也可 以为每个方法添加一个计数器,用于分别计算对每个方法的调用次数。
注意:事实上,惟一真正正确的解决方案是在 visitEnd 方法中添加更多调用,以添加新成员。实际上,
一个类中不得包含重复成员,要确保一个新成员没有重复成员,惟一方法就是将它与所有已有成员进行对 比,只有在 visitEnd 方法中访问了所有这些成员后才能完成这一工作。这种做法是相当受限制的。在
实践中,使用程序员不大可能使用的生成名,比如_counter$或_4B7F_ i 就足以避免重复成员了, 并不需要将它们添加到 visitEnd 中。注意,在第一章曾经讨论过,树 API 没有这一限制:可以在任意 时刻向使用这个 API 的转换中添加新成员。
为了举例阐述以上讨论,下面给出一个类适配器,它会向类中添加一个字段,除非这个字段 已经存在:
1 | public class AddFieldAdapter extends ClassVisitor { |
这个字段被添加在 visitEnd 方法中。visitField 方法未被重写为修改已有字段或删 除 一个字段,只是检测一下我们希望添加的字段是否已经存在。注意 visitEnd 方法中在调 用 fv.visitEnd() 之前的 fv != null 检测:这是因为一个类访问器可以在 visitField 中返 回 null
6. 转化链
到目前为止,我们已经看到一些由 ClassReader、类适配器和 ClassWriter 组成的简单 转换链。当然可以使用更为复杂的转换链,将几个类适配器链接在一起。将几个适配器链接在一 起,就可以组成几个独立的类转换,以完成复杂转换。还要注意,转换链不一定是线性的。我们可以编写一个 ClassVisitor,将接收到的所有方法调用同时转发给几个 ClassVisitor:
1 | public class MultiClassAdapter extends ClassVisitor { |
反过来,几个类适配器可以委托至同一 ClassVisitor(这需要采取一些预防措施,确保 比 如 visit 和 visitEnd 针对这个 ClassVisitor 恰好仅被调用一次)。这样一个转换链是完全可行的。

7. 工具
除了 ClassVisitor 类和相关的 ClassReader、ClassWriter 组件之外,ASM 还在 org.objectweb.asm.util包中提供了几个工具,这些工具在开发类生成器或适配器时可能非常有用,但在运行时不需要它们。ASM 还提供了一个实用类,用于在运行时处理内部名、类 型描述符和方法描述符。
7.1 类型 Type
在前几节已经看到,ASM API 公开 Java 类型的形式就是它们在已编译类中的存储形式,也 就是说,作为内部特性或类型描述符。也可以按照它们在源代码中的形式来公开它们,使代码更 便 于阅读。但这样就需要在 ClassReader 和ClassWriter 中的两种表示形式之间进行系统 转 换,从而使性能降低。这就是为什么 ASM 没有透明地将内部名和类型描述符转换为它们等价的源代码形式。但它提供了 Type 类,可以在必要时进行手动转换。
一个 Type 对象表示一种 Java 类型,既可以由类型描述符构造,也可以由 Class 对象构建。 Type 类还包含表示基元类型的静态变量。例如,Type.INT_TYPE 是表示 int 类型的 Type 对 象。
getInternalName 方 法 返 回 一 个 Type 的 内 部 名 。 例 如 ,Type.getType(String.class).getInternalName() 给出 String 类的内部名,即 “java/lang/String”。这一方法只能对类或接口类型使用。
getDescriptor 方法返回一个 Type 的描述符。比如 , 在代码中可以不使用 “**Ljava/lang/String;**” , 而 是 使 用 **Type.getType(String.class). getDescriptor()**。或者,可以不使用 I,而是使用 **Type.INT_TYPE.getDescriptor()**。
Type 对象还可以表示方法类型。这种对象既可以从一个方法描述符构建,也可以由 Method 对 象 构 建 。 getDescriptor 方 法 返 回 与 这 一 类 型 对 应 的 方 法 描 述 符 。 此 外 , getArgumentTypes 和 getReturnType 方法可用于获取与一个方法的参数类型和 返回类型 相对应的 Type 对象。例如,**Type.getArgumentTypes(“(I)V”)**返回一个仅有一个 元素 Type.INT_TYPE 的数组。与此类似 , 调用 Type.getReturnType(“(I)V”) 将返回 Type.VOID_TYPE 对象。
7.2 TraceClassVisitor
要确认所生成或转换后的类符合你的预期,ClassWriter 返回的字母数组并没有什么真正 的用处,因为它对人类来说是不可读的。如果有文本表示形式,那使用起来就容易多了。这正是 TraceClassVisitor 类提供的东西。从名字可以看出,这个类扩展了 ClassVisitor 类, 并生成所访问类的文本表示。因此, 我们不是用 ClassWriter 来生成类, 而是使用 TraceClassVisitor,以获得关于实际所生成内容的一个可读轨迹。甚至可以同时使用这两 者,这样要更好一些。除了其默认行为之外,TraceClassVisitor 实际上还可以将对其方 法 的所有调用委托给另一个访问器,比如 ClassWriter:
1 | ClassWriter cw = new ClassWriter(0); |
这一代码创建了一个 TraceClassVisitor,将它自己接收到的所有调用都委托给 cw,然 后将这些调用的一份文本表示打印到 printWriter。例如,如果在 2.2.3 节的例子中使用 TraceClassVisitor,将会得出:
1 | // 类版本号 49.0 (49) |
注意,可以在生成链或转换链的任意位置使用 TraceClassVisitor,以查看在链中这 一 点发生了什么,并非一定要恰好在 ClassWriter 之前使用。还要注意,有了这个适配器 生成 的类的文本表示形式,可能很轻松地用 String.equals() 来对比两个类。
7.3 CheckClassAdapter
ClassWriter 类并不会核实对其方法的调用顺序是否恰当,以及参数是否有效。因此,有可能会生成一些被 Java 虚拟机验证器拒绝的无效类。为了尽可能提前检测出部分此类错误,可 以使用 CheckClassAdapter 类。和 TraceClassVisitor 类似, 这个类也扩展了ClassVisitor 类,并将对其方法的所有调用都委托到另一个 ClassVisitor,比如一个TraceClassVisitor 或一个 ClassWriter。但是,这个类并不会打印所访问类的文本表示, 而是验证其对方法的调用顺序是否适当,参数是否有效,然后才会委托给下一个访问器。当发 生 错误时,会抛出 IllegalStateException或IllegalArgumentException。
为核对一个类,打印这个类的文本表示形式,最终创建一个字节数组表示形式,应当使用类 似于如下代码:
1 | ClassWriter cw = new ClassWriter(0); |
注意,如果以不同顺序将这些类访问器链在一起,那它们执行的操作也将以不同顺序完成。 例如,利用以下代码,这些核对工作将在轨迹之后进行
1 | ClassWriter cw = new ClassWriter(0); |
和使用 TraceClassVisitor 时一样,也可以在一个生成链或转换链的任意位置使用 CheckClassAdapter,以查看该链中这一点的类,而不一定只是恰好在 ClassWriter 之前使用。
7.4 ASMifier
这个类为 TraceClassVisitor 工具提供了一种替代后端(该工具在默认情况下使用Textifier 后端,生成如上所示类型的输出)。这个后端使 TraceClassVisitor 类的每个方法都会打印用于调用它的 Java 代码。例如,调用 **visitEnd()**方法将打印 cv.visitEnd();。
其结果是,当一个具有 ASMifier 后端的 TraceClassVisitor 访问器访问一个类时,它会打 印用 ASM 生成这个类的源代码。如果用这个访问器来访问一个已经存在的类,那这一点是很有用的。例如,如果你不知道如何用 ASM 生成某个已编译类,可以编写相应的源代码,用 javac 编译它,并用 ASMifier 来访问这个编译后的类。将会得到生成这个已编译类的 ASM 代码!
ASMifier 类也可以在命令行中使用。例如,使用以下命令。
1 | java -classpath asm.jar:asm-util.jar \ |
将会生成一些代码,经过缩进后,这些代码就是如下模样:
1 | package asm.java.lang; |
- 方法
本章解释如何用核心 ASM API 生成和转换已编译方法。首先介绍编译后的方法,然后介绍用于生成和转换它们的相应 ASM 接口、组件和工具,并给出大量说明性示例。
1. 结构
在编译类的内部,方法的代码存储为一系列的字节码指令。为生成和转换类,最根本的就 是 要了解这些指令,并理解它们是如何工作的。本节将对这些指令进行全面概述,这些内容足 以开 始编写简单的类生成器与转换器代码。如需完整定义,应当阅读 Java 虚拟机规范。
1.1 执行模型
在介绍字节代码指令之前,有必要先来介绍 Java 虚拟机执行模型。我们知道,Java 代码 是 在线程内部执行的。每个线程都有自己的执行栈,栈由帧组成。每个帧表示一个方法调用: 每次 调用一个方法时,会将一个新帧压入当前线程的执行栈。当方法返回时,或者是正常返 回,或者 是因为异常返回,会将这个帧从执行栈中弹出,执行过程在发出调用的方法中继续进 行(这个方 法的帧现在位于栈的顶端)。
每一帧包括两部分:一个局部变量部分和一个操作数栈部分。局部变量部分包含可根据索 引 以随机顺序访问的变量。由名字可以看出,操作数栈部分是一个栈,其中包含了供字节代码 指令用作操作数的值。这意味着这个栈中的值只能按照“后入先出”顺序访问。不要将操作数 栈和线 程的执行栈相混淆:执行栈中的每一帧都包含自己的操作数栈。
局部变量部分与操作数栈部分的大小取决于方法的代码。这一大小是在编译时计算的,并 随 字节代码指令一起存储在已编译类中。因此,对于对应于某一给定方法调用的所有帧,其局 部变 量与操作数栈部分的大小相同,但对应于不同方法的帧,这一大小可能不同。