我们都知道 java 文件需要编译成 class 文件, 然后 jvm 负责加载并运行 class 文件, 那么字节码文件长什么样子? 字节码又是怎么执行的?
工具介绍
javap
javap 是 JDK 自带的查看字节码的工具.
javap 的使用方法如下:
- $ javac Demo.java
- $ javap -p -v Demo
javap 命令打印的文件内容有时候过多, 可以使用 javap -p -v Demo>> Demo.javap 将内容追加至文本文件中, 再用文本工具打开分析.
有时候 class 文件中没有生成 LineNumberTable 或 LocalVariableTable, 可以在编译时使用下面的参数强制生成:
javac -g:lines 强制生成 LineNumberTable.
javac -g:vars 强制生成 LocalVariableTable.
javac -g 生成所有的 debug 信息.
LocalVariableTable 就是栈帧中的局部变量表.
LineNumberTable 描述源码行号与字节码行号 (字节码偏移量) 之间的对应关系, 有了这些信息, 在 debug 时, 就能够获取到发生异常的源代码行号.
jclasslib
如果你不太习惯使用命令行的操作, 还可以使用 jclasslib,jclasslib 是一个图形化的工具, 能够更加直观的查看字节码中的内容. 它还分门别类的对类中的各个部分进行了整理, 非常的人性化. 同时, 它还提供了 Idea 的插件, 你可以从 plugins 中搜索到它.
如果你在其中看不到一些诸如 LocalVariableTable 的信息, 记得在编译代码的时候加上我们上面提到的这些参数.
jclasslib 的下载地址: https://github.com/ingokegel/jclasslib
Demo.java
下面的 java 代码就是后面要分析的字节码对应的源文件:
- public class Demo {
- private int a = 1111;
- static long C = 2222;
- public long test(long num) {
- long ret = this.a + num + C;
- return ret;
- }
- public static void main(String[] args) {
- new Demo().test(3333);
- }
- }
test 方法的执行过程
Code 区域介绍
test 方法同时使用了成员变量 a, 静态变量 C, 以及输入参数 num. 我们此时说的方法执行, 内存其实就是在虚拟机栈上分配的. 下面这些内容, 就是 test 方法的字节码.
- public long test(long);
- descriptor: (J)J
- flags: ACC_PUBLIC
- Code:
- stack=4, locals=5, args_size=2
- 0: aload_0
- 1: getfield #2 // Field a:I
- 4: i2l
- 5: lload_1
- 6: ladd
- 7: getstatic #3 // Field C:J
- 10: ladd
- 11: lstore_3
- 12: lload_3
- 13: lreturn
- LineNumberTable:
- line 7: 0
- line 8: 12
说明:
stack=4: 表明了 test 方法的最大操作数栈深度为 4.JVM 运行时, 会根据这个数值, 来分配栈帧中操作栈的深度.
locals=5: 局部变量的存储空间大小, 它的单位是 Slot(槽), 可以被重用. 其中存放的内容包括: this, 方法参数, 异常处理器的参数, 方法体中定义的局部变量.
args_size=2: 方法的参数个数, 因为每个实例方法都有一个隐藏参数 this(静态方法没有 this), 所以这里的数字是 2.
字节码执行过程
0: aload_0
把第 1 个引用型局部变量推到操作数栈, 这里的意思是把 this 装载到了操作数栈中.
对于 static 方法, aload_0 表示对方法的第一个参数的操作.
image.PNG
1: getfield #2
将指定对象的第 2 个实例域 (Field) 的值, 压入栈顶.#2 就是指的我们的成员变量 a.
image.PNG
4: i2l
将栈顶 int 类型的数据转化为 long 类型, 这里就涉及我们的隐式类型转换了.
image.PNG
5: lload_1
将第一个局部变量入栈, 也就是我们的参数 num, 这里的 l 表示 long.
image.PNG
6: ladd
把栈顶两个 long 型数值出栈后相加, 并将结果入栈.
image.PNG
7: getstatic #3
根据偏移获取静态属性的值, 并把这个值 push 到操作数栈上, 也就是静态变量 C.
image.PNG
10: ladd
再次执行 ladd.
image.PNG
11: lstore_3
把栈顶 long 型数值存入第 4 个局部变量, 一个 long 和 double 类型会占用 2 个 slot.
image.PNG
这里为什么要把栈顶的变量存入局部变量表中, 又取出来入栈呢, 为什么会有这种多此一举的操作? 原因就在于我们定义了 ret 变量. JVM 不知道后面还会不会用到这个变量, 所以只好傻瓜式的顺序执行.
为了看到差异, 我们可以把代码稍微改动一下, 直接返回:
- public long test(long num) {
- return this.a + num + C;
- }
对应的字节码如下:
- public long test(long);
- descriptor: (J)J
- flags: ACC_PUBLIC
- Code:
- stack=4, locals=3, args_size=2
- 0: aload_0
- 1: getfield #2 // Field a:I
- 4: i2l
- 5: lload_1
- 6: ladd
- 7: getstatic #3 // Field C:J
- 10: ladd
- 11: lreturn
- LineNumberTable:
- line 7: 0
- 12: lload_3*
将第 3 个局部变量入栈, 也就是我们的参数 num, 这里的 l 表示 long.
image.PNG
13: lreturn
从当前方法返回 long.
来源: http://www.jianshu.com/p/2b8f8069a9f2