文章插图
(此图由开源文本编辑软件Atom的hex-view插件生成)
粗暴一点,我们可以通过 HEX 编辑器直接修改字节码,尽管这样做会有风险,但如果只修改一个数值的话应该会很有趣 。
其实要使用编程的方式,方便和安全地实现字节码编辑和修改还有更好的办法,那就是使用 ASM 和 Javassist 之类的字节码操作工具,也可以在类加载器和 Agent 上面做文章,下一节课程会讨论 类加载器,其他主题则留待以后探讨 。
4.8 对象初始化指令:new 指令, init 以及 clinit 简介
我们都知道 new是 Java 编程语言中的一个关键字,但其实在字节码中,也有一个指令叫做 new 。当我们创建类的实例时, 编译器会生成类似下面这样的操作码:
0: new #2 // class demo/jvm0104/HelloByteCode3: dup4: invokespecial #3 // Method "<init>":()V
当你同时看到 new, dup 和 invokespecial 指令在一起时,那么一定是在创建类的实例对象!为什么是三条指令而不是一条呢?这是因为:
- new 指令只是创建对象,但没有调用构造函数 。
- invokespecial 指令用来调用某些特殊方法的, 当然这里调用的是构造函数 。
- dup 指令用于复制栈顶的值 。
这就是为什么要事先复制引用的原因,为的是在构造函数返回之后,可以将对象实例赋值给局部变量或某个字段 。因此,接下来的那条指令一般是以下几种:
- astore {N} or astore_{N} – 赋值给局部变量,其中 {N} 是局部变量表中的位置 。
- putfield – 将值赋给实例字段
- putstatic – 将值赋给静态字段
还有一个可能执行的方法是该类的静态初始化方法 <clinit>,但 <clinit> 并不能被直接调用,而是由这些指令触发的: new, getstatic, putstatic or invokestatic 。
也就是说,如果创建某个类的新实例,访问静态字段或者调用静态方法,就会触发该类的静态初始化方法【如果尚未初始化】 。
实际上,还有一些情况会触发静态初始化,详情请参考 JVM 规范: [
http://docs.oracle.com/javase/specs/jvms/se8/html/]
4.9 栈内存操作指令
有很多指令可以操作方法栈 。前面也提到过一些基本的栈操作指令: 他们将值压入栈,或者从栈中获取值 。除了这些基础操作之外也还有一些指令可以操作栈内存; 比如 swap 指令用来交换栈顶两个元素的值 。下面是一些示例:
最基础的是 dup 和 pop 指令 。
- dup 指令复制栈顶元素的值 。
- pop 指令则从栈中删除最顶部的值 。
- 顾名思义,swap 指令可交换栈顶两个元素的值,例如A和B交换位置(图中示例4);
- dup_x1 将复制栈顶元素的值,并在栈顶插入两次(图中示例5);
- dup2_x1 则复制栈顶两个元素的值,并插入第三个值(图中示例6) 。
文章插图
dup_x1 和 dup2_x1 指令看起来稍微有点复杂 。而且为什么要设置这种指令呢? 在栈中复制最顶部的值?
请看一个实际案例:怎样交换 2 个 double 类型的值?
需要注意的是,一个 double 值占两个槽位,也就是说如果栈中有两个 double 值,它们将占用 4 个槽位 。
要执行交换,你可能想到了 swap 指令,但问题是 swap 只适用于单字(one-word, 单字一般指 32 位 4 个字节,64 位则是双字),所以不能处理 double 类型,但 Java 中又没有 swap2 指令 。
怎么办呢? 解决方法就是使用 dup2_x2 指令,将操作数栈顶部的 double 值,复制到栈底 double 值的下方,然后再使用 pop2 指令弹出栈顶的 double 值 。结果就是交换了两个 double 值 。示意图如下图所示:
文章插图
dup、dup_x1、dup2_x1 指令补充说明
指令的详细说明可参考 JVM 规范:
dup 指令
官方说明是:复制栈顶的值,并将复制的值压入栈 。
操作数栈的值变化情况(方括号标识新插入的值):
推荐阅读
- MyBatis自动生成工具,开发编码好帮手
- Python 常用的十行代码,建议收藏
- 绕过rar密码提取文件怎么操作?
- Java,OpenCV,图像阈值分割,阈值化,二值阈值化、截断阈值化等
- nest.js + sms 实现短信验证码登录
- 如何在开发中规避硬编码
- 十分钟搞懂手机号码一键登录
- 每天敲代码不到 1 小时,程序员每天都在做什么?
- 无代码系统搭建排班管理应用
- Java 日志记录—记录什么和不记录什么?