重庆手机网站建设,杭州化妆品网站建设,做网站彩票网站吗,同城推广有什么平台字节码新手很容易被厚厚的 JVM 书籍劝退#xff0c;即使我看过相关书籍#xff0c;工作真正用到时也全忘了#xff0c;还得现学。
等我有了一定的字节码阅读经验#xff0c;才发现字节码其实非常简单#xff0c;只需要三步就能快速学会#xff1a;
先了解 JVM 的基本结…字节码新手很容易被厚厚的 JVM 书籍劝退即使我看过相关书籍工作真正用到时也全忘了还得现学。
等我有了一定的字节码阅读经验才发现字节码其实非常简单只需要三步就能快速学会
先了解 JVM 的基本结构只需要了解字节码相关的结构即可然后将指令分成几类学习每一类是用来操作 Jvm 哪个组件的最后学习字节码的常见模式比如 new 一般会被翻译成哪几指令等等
之后按需查询 JVM指令官方文档就可以出师了。
相比官方文档本文更加适合顺序阅读读完后再按需查阅官方文档。
JVM 的基本结构 只需要了解三个和字节码相关的结构。
栈帧
JVM 每调用一个方法就会压入一个新的栈帧到栈上比如下面的例子
public static void main() {a();
}public static void a() {b();
}public static void b() {//...此处的栈如图所示...
}虽然例子中举的是静态方法但是对象方法也是一样。 重点栈帧代表一个 Java 方法里面装的是 Java 方法的运行时数据
操作数栈
每个栈帧中会有一个操作数栈这其实就是一个用来执行各种计算的工作区比如加法的执行过程
将 1 压栈将 2 压栈栈顶两个元素相加将结果 3 压栈
局部变量表
栈帧中除了用于运算的操作数栈还有用来临时存储变量的 “局部变量表”比如下面的变量 a:
public static void main() {// a 会被存入 “局部变量表”int a 10;//...此处省略1w行代码...// 将 a 从局部变量表中取出int b a 1;
}堆
操作数栈是栈帧中的存储所以其生命周期仅限于一个 Java 方法。
而堆就是能够跨越 Java 方法的存储。一般用 new 新建的对象都是在堆里然后在栈帧中只保留一个引用指针。
比如 Object a new Object(); 指令分类学习 我们可以根据操作的 JVM 组件将指令分类。每一类我们只要认识常见的几个剩下的都非常相似。
指令的结构
JVM 指令的结构和 linux 命令差不多指令 参数1 参数2 ...其中每个部分占一个字节所以叫做 “字节码”。
栈操作
dup: 复制栈顶元素add: 将栈顶两个元素出栈相加然后将结果入栈 JVM 并不存在 add 指令而是各种专门数据类型的相加指令比如 iadd 是整型相加dadd 是浮点类型相加等等其他指令也是类似的规律 ldc/bipush/const: 将常量压栈 ldc index: 将常量池中第 index 个元素入栈bipush byte此时常量不在常量池中而是作为一个 byte 被编码在了指令中const 则是一系列指令比如 iconst_0 表示将整型 0 入栈iconst_1 表示将整型 1 入栈dconst_0 表示将浮点型 0 入栈等等以此类推。可见 const 相比前两个是更加压缩的表示一个 iconst_1 同时表达了指令iconst和常量值(1)这么做的好处是可以节约一个字节。 从 add 和 const 指令中可以看出 JVM 指令会用前缀表示指令操作的数据类型比如 i 表示整型d 表示浮点型等等 局部变量表操作
局部变量表是一个数组通过下标访问。
store i: 将栈顶的值存储到局部变量表的 i 位置load i将局部变量表 i 位置的值加载到栈顶 store 和 load 也都是一系列指令比如 istore i, iload i, dstore i, dload i 等等甚至 虽然我们看到代码里的变量都是有具体的名字的比如 int a 1,b 2 中的变量 a, b。其实在运行时这些变量都会按照方法中出现的顺序被翻译成局部变量表的一个下标int a 1,b 2 对应的字节码就是
// 常量 1 入栈
0: iconst_1
// 将常量 1 存储到局部变量表的 1 位置其实就是变量 a
1: istore_1
// 常量 2 入栈
2: iconst_2
// 将常量 2 存储到局部变量表的 2 位置其实就变量 b
3: istore_2如图所示 局部变量表的 0 号位置一般是用来保存 this 引用的所以图中没有使用。 堆操作
堆主要还是依靠保存在栈中的引用来操作的。
通过 new 指令在堆中新建一个对象然后将引用保存在栈顶。
常见模式 静态方法调用
将参数入栈然后使用 invokestatic 指令调用静态方法
如下代码
static void a() {b(1,2,3);
}static void b(int a, int b, int c) {//...
}此时 a 方法的字节码就是
// 将参数入栈
0: iconst 1
1: iconst_2
2: iconst3
// 调用静态方法
3: invokestatic b:(III)V成员方法调用
将调用对象入栈将参数入栈调用成员方法
如下代码
class Test {void a() {b(1,2,3);}void b(int a, int b, int c) {}
}此时 a 方法的字节码
// 将调用对象入栈
// 回忆前文, this 引用存储在局部变量表的 0 号位置, 所以可以从局部变量表的 0 号位置加载到
0: aload_0
// 调用参数入栈
1: iconst_1
2: iconst_2
3: iconst_3
// 调用成员方法
4: invokevirtual b:(III)V成员方法的调用本质上是将对象作为隐藏的第 0 个参数比如当调用 object.a(1) 时在 JVM 层面相当于 a(object, 1)。
另外针对不同的情况invoke 有一些 invokeXxx 方法
invokeinterface调用接口方法invokevirtual: 调用对象的成员方法是最常见的invokespecial: 调用构造器等特殊方法。比如下文中的构造函数就是使用这个指令调用的
构造对象
总体步骤如下
用 new 指令新建一个空对象用 dup 复制栈顶像 “方法调用” 中一样将构造函数的参数逐个入栈invokespecial init 调用对应的构造函数完成初始化
比如下面的代码
Integer a new Integer(10);对应的指令就是
// 步骤 1.
0: new java/lang/Integer
// 步骤 2.
3: dup
// 步骤 3.
4: bipush 10
// 步骤 4.
6: invokespecial java/lang/Integer.init:(I)V前面编号代表的是指令位于第几个字节从上面可以看出除了 dup 以外其他指令都需要占用多个字节 在编码阶段对象看起来是构造好才返回的但在字节码层面其实对象新建和初始化是两个步骤先新建一个空对象然后才调用的初始化方法。
为什么需要一个 dup因为构造方法在 JVM 层面其实就是一个普通的成员方法 invokespecial 需要将对象和参数一起从栈中弹出而构造方法执行完后还需要在栈上保留一个对象因此需要 dup 保留一个副本。
分支判断
分支判断主要使用 if_xxx index 系列指令如果满足条件就跳转到 index 位置的指令执行。
如下代码
public void b(int a, int b) {int c;if (a b) {c 1;} else {c 2;}return;
}编译后的字节码是
// 加载局部变量 a
0: iload_1
// 加载局部变量 b
1: iload_2
// 如果 a b, 则跳转到位置 10 执行(其实就是 else 分支的位置)
2: if_icmple 10
5: iconst_1
6: istore_3
// 跳转到位置 12 执行(其实就是跳过 else 分支的指令)
7: goto 12
10: iconst_2
11: istore_3
12: return大体逻辑如图 做字节码实验时的注意点
当我们编写好测试的 Java 代码 Test.java可以用下面两条命令查看它的字节码
javac Test.java
javap -c -v -l Test.class需要注意的是javac 中会做一些优化导致字节码和源码对不上比如下面的代码
public static void main(String[] args) {if (2 1) {int a 2;} else {int a 1;}
}编译后字节码是
0: iconst_1
1: istore_1
2: return可以看出字节码其实只编译了 int a 1 这一行代码没有编译 if ... else ... 逻辑。这是因为 2 1 肯定是 false于是编译器就把这段逻辑优化掉了。
如何阅读字节码官方文档 这篇文章只是带大家入个门重点还是要能在遇到不认识的字节码时自主查阅 Java字节码官方文档。
比如下面的 iadd 字节码 Operation可看出是做 int 型加法操作Operand Stack表示指令执行会导致栈变化iadd 会将栈顶两个元素出栈然后将结果入栈
End 作者元青
微信公众号 「技乐书香」