虚拟机的实现方式
Java 官方的虚拟机 Hotspot 是基于栈的,而不是基于寄存器的。
- 基于栈的优点是可移植性更好、指令更短、实现起来简单,但不能随机访问栈中的元素,完成相同功能所需要的指令数也比寄存器的要多,需要频繁的入栈和出栈。 绝大多数主流的JVM(Java虚拟机)实现都是基于栈的架构
- Oracle HotSpot JVM: HotSpot是Oracle官方的JVM实现,也是最广泛使用的JVM之一。它使用基于栈的执行引擎,通过Java栈和本地方法栈来管理方法的调用和局部变量。
- OpenJ9 JVM: OpenJ9是由IBM开发的JVM实现,它也采用基于栈的架构。OpenJ9注重性能和资源利用效率,适用于嵌入式和云环境。
- Apache Harmony JVM: Apache Harmony是一个开源的Java SE项目,虽然在2011年宣布终止,但一些社区仍在维护。它使用基于栈的执行引擎。
- 基于寄存器的优点是速度快,有利于程序运行速度的优化,但操作数需要显式指定,指令也比较长。
- JRockit: JRockit是BEA公司(后被Oracle收购)开发的JVM实现,它在某些情况下使用寄存器架构来提高性能。
- Azul Systems的Zing JVM: Azul Systems的Zing JVM是一种用于大规模Java应用的商业JVM,采用了C4垃圾收集器和特定的执行引擎,其中可能包括一些寄存器架构的优化。
官方文档 jvm 规范文档 se8 class文件格式: Chapter 4. The class File Format
官方文档 jvm 规范文档 se8 指令集概要: 2.11. Instruction Set Summary
Class 结构
Java 类文件(.class 文件)是 Java 源代码编译后生成的二进制文件,它遵循特定的格式。每个类文件包含一个类或接口的定义。类文件的结构由一系列具有固定顺序的组件构成,这些组件包括:
- 魔数(Magic Number) 长度:4 字节 内容:固定的 0xCAFEBABE(十六进制),用于标识该文件是一个有效的 Java 类文件。
- 版本信息(Version Information) 长度:4 字节 分为两个部分: 次版本号(Minor Version):2 字节。 主版本号(Major Version):2 字节,表示 Java 版本。例如,Java 8 对应主版本号为 52(0x34),Java 11 对应 55(0x37)等。
- 常量池(Constant Pool) 长度:可变 常量池是类文件中的资源仓库,包含字符串常量、类和接口名、字段名、方法名等。常量池的入口是一个常量池计数(constant_pool_count),后面跟着 constant_pool_count-1 个常量项(因为常量池索引从1开始,0为保留索引)。 每个常量项的第一个字节是标签(tag),用于标识常量的类型(如:CONSTANT_Class, CONSTANT_Fieldref, CONSTANT_Methodref, CONSTANT_String 等)。
- 访问标志(Access Flags) 长度:2 字节 用于表示类或接口的访问权限和属性,例如:public、final、abstract 等。
- 当前类索引(This Class) 长度:2 字节 指向常量池中一个 CONSTANT_Class_info 结构的索引,表示当前类的全限定名。
- 父类索引(Super Class) 长度:2 字节 指向常量池中一个 CONSTANT_Class_info 结构的索引,表示父类的全限定名。对于 java.lang.Object,父类索引为0(因为它没有父类),但通常所有类都有父类,除了 Object 类。
- 接口索引集合(Interfaces) 长度:可变 包括接口计数(interfaces_count)和 interfaces_count 个接口索引(每个索引2字节),每个索引指向常量池中的 CONSTANT_Class_info。
- 字段表(Fields) 长度:可变 字段表包括字段计数(fields_count)和 fields_count 个字段信息。每个字段信息包括访问标志、名称索引、描述符索引和属性表。字段表用于描述类中声明的字段。
- 方法表(Methods) 长度:可变 方法表包括方法计数(methods_count)和 methods_count 个方法信息。每个方法信息包括访问标志、名称索引、描述符索引和属性表。方法表用于描述类中声明的方法。
- 属性表(Attributes) 长度:可变 属性表包括属性计数(attributes_count)和 attributes_count 个属性信息。属性可以出现在类、字段和方法中。常见的属性有:Code(方法的字节码)、LineNumberTable(行号表)、SourceFile(源文件名称)等。
Java 大部分字节码指令
2.6.1. Local Variables 2.6.2. Operand Stacks 2.6.3. Dynamic Linking 2.6.4. Normal Method Invocation Completion 2.6.5. Abrupt Method Invocation Completion Java字节码指令大全
指令 常量入栈
| 指令码 | 操作码(助记符) | 操作数 | 描述(栈指操作数栈) |
|---|---|---|---|
| 0x01 | aconst_null | null值入栈; | |
| 0x02 | iconst_m1 | (int)值 -1入栈; | |
| 0x03 | iconst_0 | (int)值 0入栈; | |
| 0x09 | lconst_0 | (long)值 0入栈; | |
| 0x0a | lconst_1 | 1(long)值入栈; | |
| 0x0b | fconst_0 | 0(float)值入栈; | |
| 0x0c | fconst_1 | 1(float)值入栈; | |
| 0x0d | fconst_2 | 2(float)值入栈; | |
| 0x0e | dconst_0 | 0(double)值入栈; | |
| 0x0f | dconst_1 | 1(double)值入栈; | |
| 0x10 | bipush | valuebyte | valuebyte值带符号扩展成int值入栈; |
| 0x11 | sipush | valuebyte1 | (valuebyte1 << 8) |
| valuebyte2 | valuebyte2 值带符号扩展成int值入栈; | ||
| 0x12 | ldc | indexbyte1 | 常量池中的常量值(int, float, string reference, object reference)入栈; |
| 0x13 | ldc_w | indexbyte1 | 常量池中常量(int, float, string reference, object reference)入栈; |
| indexbyte2 | |||
| 0x14 | ldc2_w | indexbyte1 | 常量池中常量(long, double)入栈; |
| indexbyte2 |
push 系列,主要包括 bipush 和 sipush,前者接收 8 位整数作为参数,后者接收 16 位整数。 Idc 指令,当 const 和 push 不能满足的时候,万能的 Idc 指令就上场了,它接收一个 8 位的参数,指向常量池中的索引。
指令 局部变量入栈
| 指令码 | 操作码(助记符) | 操作数 | 描述(栈指操作数栈) |
|---|---|---|---|
| 0x19 | (wide)aload | indexbyte | 从局部变量indexbyte中装载引用类型值入栈; |
| 0x2a | aload_0 | 从局部变量0中装载引用类型值入栈; | |
| 0x2b | aload_1 | 从局部变量1中装载引用类型值入栈; | |
| 0x2c | aload_2 | 从局部变量2中装载引用类型值入栈; | |
| 0x2d | aload_3 | 从局部变量3中装载引用类型值入栈; | |
| 0x15 | (wide)iload | indexbyte | 从局部变量indexbyte中装载int类型值入栈; |
| 0x1a | iload_0 | 从局部变量0中装载int类型值入栈; | |
| 0x16 | (wide)lload | indexbyte | 从局部变量indexbyte中装载long类型值入栈; |
| 0x1e | lload_0 | 从局部变量0中装载int类型值入栈; | |
| 0x17 | (wide)fload | indexbyte | 从局部变量indexbyte中装载float类型值入栈; |
| 0x22 | fload_0 | 从局部变量0中装载float类型值入栈; | |
| 0x18 | (wide)dload | indexbyte | 从局部变量indexbyte中装载double类型值入栈; |
| 0x26 | dload_0 | 从局部变量0中装载double类型值入栈; | |
| 0x32 | aaload | 从引用类型数组中装载指定项的值; | |
| 0x2e | iaload | 从int类型数组中装载指定项的值; | |
| 0x2f | laload | 从long类型数组中装载指定项的值; | |
| 0x30 | faload | 从float类型数组中装载指定项的值; | |
| 0x31 | daload | 从double类型数组中装载指定项的值; | |
| 0x33 | baload | 从boolean类型数组或byte类型数组中装载指定项的值(先转换为int类型值, 后压栈); | |
| 0x34 | caload | 从char类型数组中装载指定项的值(先转换为int类型值, 后压栈); | |
| 0x35 | saload | 从short类型数组中装载指定项的值(先转换为int类型值, 后压栈); |
{X}load_{N}(X 为 i(int)、l(long)、f(float)、d(double)、a(引用类型); N 默认为 0 到 3),表示将第 n 个局部变量压入操作数栈中。
大部分的指令都不支持 byte、short 和 char,甚至没有任何指令支持 boolean 类型。编译器会将 byte 和 short 类型的数据带符号扩展(Sign-Extend)为 int 类型,将 boolean 和 char 零位扩展(Zero-Extend)为 int 类型。
指令 将栈顶值保存到局部变量
| 指令码 | 操作码(助记符) | 操作数 | 描述(栈指操作数栈) |
|---|---|---|---|
| 0x3a | (wide)astore | indexbyte | 将栈顶引用类型值保存到局部变量indexbyte中; |
| 0x4b | astroe_0 | 将栈顶引用类型值保存到局部变量0中; | |
| 0x4c | astore_1 | 将栈顶引用类型值保存到局部变量1中; | |
| 0x4d | astore_2 | 将栈顶引用类型值保存到局部变量2中; | |
| 0x4e | astore_3 | 将栈顶引用类型值保存到局部变量3中; | |
| 0x36 | (wide)istore | indexbyte | 将栈顶int类型值保存到局部变量indexbyte中; |
| 0x3b | istore_0 | 将栈顶int类型值保存到局部变量0中; | |
| 0x3c | istore_1 | 将栈顶int类型值保存到局部变量1中; | |
| 0x3d | istore_2 | 将栈顶int类型值保存到局部变量2中; | |
| 0x3e | istore_3 | 将栈顶int类型值保存到局部变量3中; | |
| 0x37 | (wide)lstore | indexbyte | 将栈顶long类型值保存到局部变量indexbyte中; |
| 0x3f | lstore_0 | 将栈顶long类型值保存到局部变量0中; | |
| 0x40 | lstore_1 | 将栈顶long类型值保存到局部变量1中; | |
| 0x41 | lstore_2 | 将栈顶long类型值保存到局部变量2中; | |
| 0x42 | lstroe_3 | 将栈顶long类型值保存到局部变量3中; | |
| 0x38 | (wide)fstore | indexbyte | 将栈顶float类型值保存到局部变量indexbyte中; |
| 0x43 | fstore_0 | 将栈顶float类型值保存到局部变量0中; | |
| 0x44 | fstore_1 | 将栈顶float类型值保存到局部变量1中; | |
| 0x45 | fstore_2 | 将栈顶float类型值保存到局部变量2中; | |
| 0x46 | fstore_3 | 将栈顶float类型值保存到局部变量3中; | |
| 0x39 | (wide)dstore | indexbyte | 将栈顶double类型值保存到局部变量indexbyte中; |
| 0x47 | dstore_0 | 将栈顶double类型值保存到局部变量0中; | |
| 0x48 | dstore_1 | 将栈顶double类型值保存到局部变量1中; | |
| 0x49 | dstore_2 | 将栈顶double类型值保存到局部变量2中; | |
| 0x4a | dstore_3 | 将栈顶double类型值保存到局部变量3中; | |
| 0x53 | aastore | 将栈顶引用类型值保存到指定引用类型数组的指定项; | |
| 0x4f | iastore | 将栈顶int类型值保存到指定int类型数组的指定项; | |
| 0x50 | lastore | 将栈顶long类型值保存到指定long类型数组的指定项; | |
| 0x51 | fastore | 将栈顶float类型值保存到指定float类型数组的指定项; | |
| 0x52 | dastore | 将栈顶double类型值保存到指定double类型数组的指定项; | |
| 0x54 | bastroe | 将栈顶boolean类型值或byte类型值保存到指定boolean类型数组或byte类型数组的指定项; | |
| 0x55 | castore | 将栈顶char类型值保存到指定char类型数组的指定项; | |
| 0x56 | sastore | 将栈顶short类型值保存到指定short类型数组的指定项; |
指令 通用(无类型)栈操作
| 指令码 | 操作码(助记符) | 操作数 | 描述(栈指操作数栈) |
|---|---|---|---|
| 0x00 | nop | 空操作; | |
| 0x57 | pop | 从栈顶弹出一个字长的数据; | |
| 0x58 | pop2 | 从栈顶弹出两个字长的数据; | |
| 0x59 | dup | 复制栈顶一个字长的数据, 将复制后的数据压栈; | |
| 0x5a | dup_x1 | 复制栈顶一个字长的数据, 弹出栈顶两个字长数据, 先将复制后的数据压栈, 再将弹出的两个字长数据压栈; | |
| 0x5b | dup_x2 | 复制栈顶一个字长的数据, 弹出栈顶三个字长的数据, 将复制后的数据压栈, 再将弹出的三个字长的数据压栈; | |
| 0x5c | dup2 | 复制栈顶两个字长的数据, 将复制后的两个字长的数据压栈; | |
| 0x5d | dup2_x1 | 复制栈顶两个字长的数据, 弹出栈顶三个字长的数据, 将复制后的两个字长的数据压栈, 再将弹出的三个字长的数据压栈; | |
| 0x5e | dup2_x2 | 复制栈顶两个字长的数据, 弹出栈顶四个字长的数据, 将复制后的两个字长的数据压栈, 再将弹出的四个字长的数据压栈; | |
| 0x5f | swap | 交换栈顶两个字长的数据的位置; Java指令中没有提供以两个字长为单位的交换指令; |
指令 控制流
条件跳转
| 指令码 | 操作码(助记符) | 操作数 | 描述(栈指操作数栈) |
|---|---|---|---|
| 0x99 | ifeq | branchbyte1 | 若栈顶int类型值为0则跳转; |
| branchbyte2 | |||
| 0x9a | ifne | branchbyte1 | 若栈顶int类型值不为0则跳转; |
| branchbyte2 | |||
| 0x9b | iflt | branchbyte1 | 若栈顶int类型值小于0则跳转; |
| branchbyte2 | |||
| 0x9e | ifle | branchbyte1 | 若栈顶int类型值小于等于0则跳转; |
| branchbyte2 | |||
| 0x9d | ifgt | branchbyte1 | 若栈顶int类型值大于0则跳转; |
| branchbyte2 | |||
| 0x9c | ifge | branchbyte1 | 若栈顶int类型值大于等于0则跳转; |
| branchbyte2 | |||
| 0x9f | if_icmpeq | branchbyte1 | 若栈顶两int类型值相等则跳转; |
| branchbyte2 | |||
| 0xa0 | if_icmpne | branchbyte1 | 若栈顶两int类型值不相等则跳转; |
| branchbyte2 | |||
| 0xa1 | if_icmplt | branchbyte1 | 若栈顶两int类型值前小于后则跳转; |
| branchbyte2 | |||
| 0xa4 | if_icmple | branchbyte1 | 若栈顶两int类型值前小于等于后则跳转; |
| branchbyte2 | |||
| 0xa3 | if_icmpgt | branchbyte1 | 若栈顶两int类型值前大于后则跳转; |
| branchbyte2 | |||
| 0xa2 | if_icmpge | branchbyte1 | 若栈顶两int类型值前大于等于后则跳转; |
| branchbyte2 | |||
| 0xc6 | ifnull | branchbyte1 | 若栈顶引用值为null则跳转; |
| branchbyte2 | |||
| 0xc7 | ifnonnull | branchbyte1 | 若栈顶引用值不为null则跳转; |
| branchbyte2 | |||
| 0xa5 | if_acmpeq | branchbyte1 | 若栈顶两引用类型值相等则跳转; |
| branchbyte2 | |||
| 0xa6 | if_acmpne | branchbyte1 | 若栈顶两引用类型值不相等则跳转; |
| branchbyte2 | |||
栈顶比较
| 指令码 | 操作码(助记符) | 操作数 | 描述(栈指操作数栈) |
|---|---|---|---|
| 0x94 | lcmp | 比较栈顶两long类型值, 前者大, 1入栈; 相等, 0入栈; 后者大, -1入栈; | |
| 0x95 | fcmpl | 比较栈顶两float类型值, 前者大, 1入栈; 相等, 0入栈; 后者大, -1入栈; 有NaN存在, -1入栈; | |
| 0x96 | fcmpg | 比较栈顶两float类型值, 前者大, 1入栈; 相等, 0入栈; 后者大, -1入栈; 有NaN存在, -1入栈; | |
| 0x97 | dcmpl | 比较栈顶两double类型值, 前者大, 1入栈; 相等, 0入栈; 后者大, -1入栈; 有NaN存在, -1入栈; | |
| 0x98 | dcmpg | 比较栈顶两double类型值, 前者大, 1入栈; 相等, 0入栈; 后者大, -1入栈; 有NaN存在, -1入栈; |
无条件跳转
| 指令码 | 操作码(助记符) | 操作数 | 描述(栈指操作数栈) |
|---|---|---|---|
| 0xa7 | goto | branchbyte1 | 无条件跳转到指定位置; |
| branchbyte2 | |||
| 0xc8 | goto_w | branchbyte1 | 无条件跳转到指定位置(宽索引); |
| branchbyte2 | |||
| branchbyte3 | |||
| branchbyte4 | |||
指令 对象操作
| 指令码 | 操作码(助记符) | 操作数 | 描述(栈指操作数栈) |
|---|---|---|---|
| 0xbb | new | indexbyte1 | 创建新的对象实例; |
| indexbyte2 | |||
| 0xc0 | checkcast | indexbyte1 | 类型强转; |
| indexbyte | |||
| 0xc1 | instanceof | indexbyte1 | 判断类型; |
| indexbyte2 | |||
| 0xb4 | getfield | indexbyte1 | 获取对象字段的值; |
| indexbyte2 | |||
| 0xb5 | putfield | indexbyte1 | 给对象字段赋值; |
| indexbyte2 | |||
| 0xb2 | getstatic | indexbyte1 | 获取静态字段的值; |
| indexbyte2 | |||
| 0xb3 | putstatic | indexbyte1 | 给静态字段赋值; |
| indexbyte2 |
指令 方法调用
| 指令码 | 操作码(助记符) | 操作数 | 描述(栈指操作数栈) |
|---|---|---|---|
| 0xb7 | invokespecial | indexbyte1 | 编译时方法绑定调用方法; |
| indexbyte2 | |||
| 0xb6 | invokevirtual | indexbyte1 | 运行时方法绑定调用方法; |
| indexbyte2 | |||
| 0xb8 | invokestatic | indexbyte1 | 调用静态方法; |
| indexbyte2 | |||
| 0xb9 | invokeinterface | indexbyte1 | 调用接口方法; |
| indexbyte2 | |||
| count | |||
| 0 |
指令 方法返回
| 指令码 | 操作码(助记符) | 操作数 | 描述(栈指操作数栈) |
|---|---|---|---|
| 0xac | ireturn | 返回int类型值; | |
| 0xad | lreturn | 返回long类型值; | |
| 0xae | freturn | 返回float类型值; | |
| 0xaf | dreturn | 返回double类型值; | |
| 0xb0 | areturn | 返回引用类型值; | |
| 0xb1 | return | void函数返回; |
骚操作 - 无源码如何修改class?
javassist 修改class 文件
Javassist是一个开源的分析, 编辑和创建Java字节码的类库; 是由东京技术学院的数学和计算机科学系的 Shigeru Chiba 所创建的; 它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态AOP框架;
- 构建
ClassPool对象, 它表示一个class
ClassPool pool = ClassPool.getDefault();
- 创建一个 class 返回 CtClass
CtClass ctClass = pool.makeClass("top.ss007.GenerateClass");//创建
从已有的 class 文件 构建 CtClass //必须将class文件放在这个工程编译后的class文件中, 路径也对应起来, 或者使用 insertClassPath 指定
pool.insertClassPath("F:\\test\\饲料_jhw\\classes\\");
CtClass ctClass = pool.get("com.rickiyang.learn.javassist.PersonService");- //写入到文件
ctClass.writeFile("D:\\test");
ClassPool需要关注的方法:
getDefault: 返回默认的ClassPool 是单例模式的, 一般通过该方法创建我们的ClassPool;
appendClassPath, insertClassPath: 将一个ClassPath加到类搜索路径的末尾位置 或 插入到起始位置; 通常通过该方法写入额外的类搜索路径, 以解决多个类加载器环境中找不到类的尴尬;
toClass: 将修改后的CtClass加载至当前线程的上下文类加载器中, CtClass的toClass方法是通过调用本方法实现; 需要注意的是一旦调用该方法, 则无法继续修改已经被加载的class;
get , getCtClass: 根据类路径名获取该类的CtClass对象, 用于后续的编辑;
CtClass 需要关注的方法:
freeze: 冻结一个类, 使其不可修改;
isFrozen: 判断一个类是否已被冻结;
prune: 删除类不必要的属性, 以减少内存占用; 调用该方法后, 许多方法无法将无法正常使用, 慎用;
defrost: 解冻一个类, 使其可以被修改; 如果事先知道一个类会被defrost, 则禁止调用 prune 方法;
detach: 将该class从ClassPool中删除;
writeFile: 根据CtClass生成 .class 文件;
toClass: 通过类加载器加载该CtClass;
上面我们创建一个新的方法使用了CtMethod类; CtMthod代表类中的某个方法, 可以通过CtClass提供的API获取或者CtNewMethod新建, 通过CtMethod对象可以实现对方法的修改;
CtMethod中的一些重要方法:
insertBefore: 在方法的起始位置插入代码;
insterAfter: 在方法的所有 return 语句前插入代码以确保语句能够被执行, 除非遇到exception;
insertAt: 在指定的位置插入代码;
setBody: 将方法的内容设置为要写入的代码, 当方法被 abstract修饰时, 该修饰符被移除;
make: 创建一个新的方法;
另外需要注意的是: 上面的insertBefore() 和 setBody()中的语句, 如果你是单行语句可以直接用双引号, 但是有多行语句的情况下, 你需要将多行语句用{}括起来; javassist 只接受单个语句或用大括号括起来的语句块;
javassist使用全解析
上下文环境参数
传递给方法 insertBefore() , insertAfter() , addCatch() 和 insertAt() 的 String 对象是由Javassist 的编译器编译的;
由于编译器支持语言扩展, 以 $ 开头的几个标识符有特殊的含义:
符号 含义
$0, $1, $2, ... this and 方法的参数
$args 方法参数数组.它的类型为 Object[]
$$ 所有实参; 例如, m($$) 等价于 m($1,$2,...)
$cflow(...) cflow 变量
$r 返回结果的类型, 用于强制类型转换
$w 包装器类型, 用于强制类型转换
$_ 返回值
$sig 类型为 java.lang.Class 的参数类型数组
$type 一个 java.lang.Class 对象, 表示返回值类型
$class 一个 java.lang.Class 对象, 表示当前正在修改的类这种方法有限制:
- 需要环境上下文
- 只能在特定位置插入代码
javassist 和 CGLIB
-
javassit 直接使用java编码的形式, 而不需要了解虚拟机指令, 就能动态改变类的结构, 或者动态生成类; 它是直接修改class 本质是静态
-
cglib 动态代理类的模式是:
-
查找目标类上的所有非final 的public类型的方法定义
-
将这些方法的定义转换成字节码;
-
将组成的字节码转换成相应的代理的class对象
-
实现 MethodInterceptor接口, 用来处理 对代理类上所有方法的请求(这个接口和JDK动态代理InvocationHandler的功能和角色是一样的)
Recaf
真正牛逼的字节码指令修改器, 直接改指令, (2.0 是JDK9+的..)
简单分析一则
//源码
public class Test {
public void Test001() {
String string = new String("acc");
System.out.println(string);
}
}
// Test001 被编译后分析的指令
0: LABEL A
1: LINE 18:LABEL A
2: NEW String//创建新的String对象实例
3: DUP//复制栈顶一个字长的数据, 将复制后的数据压栈; 见下 [NEW 指令之后为什么需要 DUP?]
4: LDC "acc"//常量acc入栈
5: INVOKESPECIAL String.<init>(String)void//编译时方法绑定调用, 调用String(String)的构造函数
6: ASTORE 1 [string:String]//将栈顶引用类型值保存到局部 变量1 中;
7: LABEL B
8: LINE 19:LABEL B
9: GETSTATIC System.out PrintStream//获取 System.out的PrintStream 值
10: ALOAD 1 [string:String]//装载局部 变量1 中的引用类型值入栈
11: INVOKEVIRTUAL PrintStream.println(String)void//运行时方法绑定调用, 调用println方法
12: LABEL C
13: LINE 20:LABEL C
14: RETURN //void函数返回
15: LABEL D关于 LABEL *LINE 18:LABEL* 应该是recaf 自动生成的 操作分隔符, 便于分析?
在11行 11: INVOKEVIRTUAL PrintStream.println(String)void 右键 →show stack/locals 可以看到栈局部变量表
| index | Local source opcode |
|---|---|
| 0 | PARAM 0 (this:Test) |
| 1 | 2: NEW String |
| 6:ASTORE 1 [string:String] |
栈索引0 是 this 作用域 栈索引1 一个string:String 变量, 在2行创建 在6行入栈
NEW 指令之后为什么需要 DUP指令?
因为new指令之后, 紧跟着就会调用指令 invokespecial 进行初始化(会消耗掉操作数栈顶的引用作为传给构造器的”this”参数)
invokespecial的指令格式; 看一下操作数栈, 需要一个objectref引用(对象的地址), 后面是可选的参数; 由于初始化没有返回值
in short (invokespecial)构造函数没有返回值; 那么怎么获取构造完毕的对象呢?只能在调用构造函数前把对象句柄复制一份
so.. 简单理解为:
NEW String//创建对象 ‘this’ 引用入栈DUP//再复制一份 ‘this’ 入栈LDC "acc"INVOKESPECIAL String.<init>(String)void// 调用构造 操作数栈顶 ‘DUP’的那份ASTORE 1 [string:String]//保存, 消耗掉NEW的那份 栈顶
保存修改/导出
File -> Export
jclasslib 查看
jclasslib bytecode viewer is a tool that visualizes all aspects of compiled Java class files and the contained bytecode. In addition, it contains a library that enables developers to read and write Java class files and bytecode.
只能看不能编辑指令; 但可以编辑常量池信息;